@uniai-fe/uds-primitives 0.0.1

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 (217) hide show
  1. package/README.md +63 -0
  2. package/package.json +85 -0
  3. package/src/components/alternate/hooks/index.ts +4 -0
  4. package/src/components/alternate/img/.gitkeep +0 -0
  5. package/src/components/alternate/index.scss +1 -0
  6. package/src/components/alternate/index.tsx +4 -0
  7. package/src/components/alternate/markup/index.tsx +4 -0
  8. package/src/components/alternate/styles/index.scss +3 -0
  9. package/src/components/alternate/types/index.ts +4 -0
  10. package/src/components/alternate/utils/index.ts +4 -0
  11. package/src/components/badge/hooks/index.ts +4 -0
  12. package/src/components/badge/img/.gitkeep +0 -0
  13. package/src/components/badge/index.scss +1 -0
  14. package/src/components/badge/index.tsx +6 -0
  15. package/src/components/badge/markup/Badge.tsx +51 -0
  16. package/src/components/badge/markup/index.tsx +1 -0
  17. package/src/components/badge/styles/index.scss +189 -0
  18. package/src/components/badge/types/index.ts +55 -0
  19. package/src/components/badge/utils/index.ts +21 -0
  20. package/src/components/button/hooks/index.ts +4 -0
  21. package/src/components/button/img/.gitkeep +0 -0
  22. package/src/components/button/index.scss +1 -0
  23. package/src/components/button/index.tsx +6 -0
  24. package/src/components/button/markup/Button.tsx +175 -0
  25. package/src/components/button/markup/index.tsx +1 -0
  26. package/src/components/button/styles/index.scss +847 -0
  27. package/src/components/button/types/index.ts +79 -0
  28. package/src/components/button/utils/index.ts +58 -0
  29. package/src/components/calendar/hooks/index.ts +4 -0
  30. package/src/components/calendar/img/.gitkeep +0 -0
  31. package/src/components/calendar/index.scss +1 -0
  32. package/src/components/calendar/index.tsx +4 -0
  33. package/src/components/calendar/markup/index.tsx +4 -0
  34. package/src/components/calendar/styles/index.scss +3 -0
  35. package/src/components/calendar/types/index.ts +4 -0
  36. package/src/components/calendar/utils/index.ts +4 -0
  37. package/src/components/checkbox/hooks/index.ts +4 -0
  38. package/src/components/checkbox/img/.gitkeep +0 -0
  39. package/src/components/checkbox/img/check-large.svg +3 -0
  40. package/src/components/checkbox/img/check-medium.svg +3 -0
  41. package/src/components/checkbox/img/check.svg +3 -0
  42. package/src/components/checkbox/index.scss +1 -0
  43. package/src/components/checkbox/index.tsx +4 -0
  44. package/src/components/checkbox/markup/Checkbox.tsx +127 -0
  45. package/src/components/checkbox/markup/index.ts +1 -0
  46. package/src/components/checkbox/styles/index.scss +164 -0
  47. package/src/components/checkbox/types/checkbox.ts +21 -0
  48. package/src/components/checkbox/types/index.ts +1 -0
  49. package/src/components/chip/hooks/index.ts +4 -0
  50. package/src/components/chip/img/.gitkeep +0 -0
  51. package/src/components/chip/img/remove.svg +3 -0
  52. package/src/components/chip/index.scss +1 -0
  53. package/src/components/chip/index.tsx +6 -0
  54. package/src/components/chip/markup/Chip.tsx +103 -0
  55. package/src/components/chip/markup/index.tsx +1 -0
  56. package/src/components/chip/styles/index.scss +140 -0
  57. package/src/components/chip/types/index.ts +52 -0
  58. package/src/components/chip/utils/index.ts +36 -0
  59. package/src/components/dialog/hooks/index.ts +4 -0
  60. package/src/components/dialog/img/.gitkeep +0 -0
  61. package/src/components/dialog/index.scss +1 -0
  62. package/src/components/dialog/index.tsx +3 -0
  63. package/src/components/dialog/markup/confirm-dialog.tsx +316 -0
  64. package/src/components/dialog/markup/index.tsx +4 -0
  65. package/src/components/dialog/markup/notice-dialog.tsx +191 -0
  66. package/src/components/dialog/styles/base.scss +153 -0
  67. package/src/components/dialog/styles/confirm.scss +58 -0
  68. package/src/components/dialog/styles/index.scss +3 -0
  69. package/src/components/dialog/styles/notice.scss +65 -0
  70. package/src/components/dialog/types/index.ts +70 -0
  71. package/src/components/dialog/utils/index.ts +4 -0
  72. package/src/components/drawer/hooks/index.ts +113 -0
  73. package/src/components/drawer/img/.gitkeep +0 -0
  74. package/src/components/drawer/img/close.svg +3 -0
  75. package/src/components/drawer/index.scss +1 -0
  76. package/src/components/drawer/index.tsx +3 -0
  77. package/src/components/drawer/markup/drawer.tsx +421 -0
  78. package/src/components/drawer/markup/index.tsx +3 -0
  79. package/src/components/drawer/styles/index.scss +232 -0
  80. package/src/components/drawer/types/index.ts +51 -0
  81. package/src/components/drawer/utils/context.ts +15 -0
  82. package/src/components/drawer/utils/index.tsx +77 -0
  83. package/src/components/dropdown/hooks/index.ts +4 -0
  84. package/src/components/dropdown/img/.gitkeep +0 -0
  85. package/src/components/dropdown/index.scss +1 -0
  86. package/src/components/dropdown/index.tsx +4 -0
  87. package/src/components/dropdown/markup/index.tsx +4 -0
  88. package/src/components/dropdown/styles/index.scss +3 -0
  89. package/src/components/dropdown/types/index.ts +4 -0
  90. package/src/components/dropdown/utils/index.ts +4 -0
  91. package/src/components/input/hooks/index.ts +4 -0
  92. package/src/components/input/img/.gitkeep +0 -0
  93. package/src/components/input/img/check-correct.svg +3 -0
  94. package/src/components/input/img/check-default.svg +3 -0
  95. package/src/components/input/img/check-incorrect.svg +3 -0
  96. package/src/components/input/img/error.svg +5 -0
  97. package/src/components/input/img/hide-off.svg +4 -0
  98. package/src/components/input/img/hide-on.svg +6 -0
  99. package/src/components/input/img/reset.svg +3 -0
  100. package/src/components/input/img/search.svg +4 -0
  101. package/src/components/input/img/success.svg +3 -0
  102. package/src/components/input/index.scss +1 -0
  103. package/src/components/input/index.tsx +6 -0
  104. package/src/components/input/markup/index.tsx +1 -0
  105. package/src/components/input/markup/text/Base.tsx +311 -0
  106. package/src/components/input/markup/text/Identification.tsx +145 -0
  107. package/src/components/input/markup/text/Password.tsx +71 -0
  108. package/src/components/input/markup/text/Phone.tsx +115 -0
  109. package/src/components/input/markup/text/Search.tsx +35 -0
  110. package/src/components/input/markup/text/index.ts +10 -0
  111. package/src/components/input/styles/index.scss +375 -0
  112. package/src/components/input/types/index.ts +56 -0
  113. package/src/components/input/utils/index.ts +54 -0
  114. package/src/components/label/hooks/index.ts +4 -0
  115. package/src/components/label/img/.gitkeep +0 -0
  116. package/src/components/label/index.scss +1 -0
  117. package/src/components/label/index.tsx +4 -0
  118. package/src/components/label/markup/index.tsx +4 -0
  119. package/src/components/label/styles/index.scss +3 -0
  120. package/src/components/label/types/index.ts +4 -0
  121. package/src/components/label/utils/index.ts +4 -0
  122. package/src/components/navigation/hooks/index.ts +4 -0
  123. package/src/components/navigation/img/.gitkeep +0 -0
  124. package/src/components/navigation/index.scss +1 -0
  125. package/src/components/navigation/index.tsx +8 -0
  126. package/src/components/navigation/markup/index.tsx +2 -0
  127. package/src/components/navigation/markup/mobile/BottomNavigation.tsx +127 -0
  128. package/src/components/navigation/markup/mobile/index.ts +1 -0
  129. package/src/components/navigation/markup/web/index.ts +4 -0
  130. package/src/components/navigation/styles/index.scss +133 -0
  131. package/src/components/navigation/types/index.ts +38 -0
  132. package/src/components/navigation/utils/index.ts +23 -0
  133. package/src/components/pagination/hooks/index.ts +4 -0
  134. package/src/components/pagination/img/.gitkeep +0 -0
  135. package/src/components/pagination/index.scss +1 -0
  136. package/src/components/pagination/index.tsx +6 -0
  137. package/src/components/pagination/markup/Carousel.tsx +76 -0
  138. package/src/components/pagination/markup/Count.tsx +54 -0
  139. package/src/components/pagination/markup/Pagination.tsx +83 -0
  140. package/src/components/pagination/markup/index.tsx +3 -0
  141. package/src/components/pagination/styles/index.scss +155 -0
  142. package/src/components/pagination/types/index.ts +68 -0
  143. package/src/components/pagination/utils/index.ts +58 -0
  144. package/src/components/radio/hooks/index.ts +4 -0
  145. package/src/components/radio/img/.gitkeep +0 -0
  146. package/src/components/radio/index.scss +1 -0
  147. package/src/components/radio/index.tsx +7 -0
  148. package/src/components/radio/markup/Radio.tsx +121 -0
  149. package/src/components/radio/markup/RadioCard.tsx +68 -0
  150. package/src/components/radio/markup/RadioCardGroup.tsx +75 -0
  151. package/src/components/radio/markup/index.tsx +3 -0
  152. package/src/components/radio/styles/index.scss +252 -0
  153. package/src/components/radio/types/index.ts +1 -0
  154. package/src/components/radio/types/radio.ts +63 -0
  155. package/src/components/radio/utils/index.ts +4 -0
  156. package/src/components/scrollbar/hooks/index.ts +4 -0
  157. package/src/components/scrollbar/img/.gitkeep +0 -0
  158. package/src/components/scrollbar/index.scss +1 -0
  159. package/src/components/scrollbar/index.tsx +4 -0
  160. package/src/components/scrollbar/markup/index.tsx +4 -0
  161. package/src/components/scrollbar/styles/index.scss +3 -0
  162. package/src/components/scrollbar/types/index.ts +4 -0
  163. package/src/components/scrollbar/utils/index.ts +4 -0
  164. package/src/components/segmented-control/index.scss +1 -0
  165. package/src/components/segmented-control/index.tsx +7 -0
  166. package/src/components/segmented-control/markup/SegmentedControl.tsx +117 -0
  167. package/src/components/segmented-control/markup/index.ts +1 -0
  168. package/src/components/segmented-control/styles/index.scss +113 -0
  169. package/src/components/segmented-control/types/index.ts +22 -0
  170. package/src/components/select/hooks/index.ts +4 -0
  171. package/src/components/select/img/.gitkeep +0 -0
  172. package/src/components/select/index.scss +1 -0
  173. package/src/components/select/index.tsx +4 -0
  174. package/src/components/select/markup/index.tsx +4 -0
  175. package/src/components/select/styles/index.scss +3 -0
  176. package/src/components/select/types/index.ts +4 -0
  177. package/src/components/select/utils/index.ts +4 -0
  178. package/src/components/spinner/hooks/index.ts +4 -0
  179. package/src/components/spinner/img/.gitkeep +0 -0
  180. package/src/components/spinner/index.scss +1 -0
  181. package/src/components/spinner/index.tsx +4 -0
  182. package/src/components/spinner/markup/index.tsx +4 -0
  183. package/src/components/spinner/styles/index.scss +3 -0
  184. package/src/components/spinner/types/index.ts +4 -0
  185. package/src/components/spinner/utils/index.ts +4 -0
  186. package/src/components/tab/hooks/index.ts +4 -0
  187. package/src/components/tab/img/.gitkeep +0 -0
  188. package/src/components/tab/index.scss +1 -0
  189. package/src/components/tab/index.tsx +6 -0
  190. package/src/components/tab/markup/TabContent.tsx +29 -0
  191. package/src/components/tab/markup/TabList.tsx +60 -0
  192. package/src/components/tab/markup/TabRoot.tsx +74 -0
  193. package/src/components/tab/markup/TabTrigger.tsx +47 -0
  194. package/src/components/tab/markup/index.tsx +4 -0
  195. package/src/components/tab/styles/index.scss +182 -0
  196. package/src/components/tab/types/index.ts +46 -0
  197. package/src/components/tab/utils/index.ts +5 -0
  198. package/src/components/tab/utils/tab-context.ts +20 -0
  199. package/src/components/table/hooks/index.ts +4 -0
  200. package/src/components/table/img/.gitkeep +0 -0
  201. package/src/components/table/index.scss +1 -0
  202. package/src/components/table/index.tsx +4 -0
  203. package/src/components/table/markup/index.tsx +4 -0
  204. package/src/components/table/styles/index.scss +3 -0
  205. package/src/components/table/types/index.ts +4 -0
  206. package/src/components/table/utils/index.ts +4 -0
  207. package/src/hooks/index.ts +4 -0
  208. package/src/img/.gitkeep +0 -0
  209. package/src/index.scss +3 -0
  210. package/src/index.tsx +26 -0
  211. package/src/init/dayjs.ts +14 -0
  212. package/src/theme/ThemeProvider.tsx +25 -0
  213. package/src/theme/config.ts +29 -0
  214. package/src/theme/index.ts +3 -0
  215. package/src/theme/overrides.scss +215 -0
  216. package/src/types/index.ts +4 -0
  217. package/src/utils/index.ts +4 -0
@@ -0,0 +1,79 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode, ReactElement } from "react";
2
+
3
+ /**
4
+ * 버튼 variant/axes는 `modules/_context/design-system/sot/current/components/assets-index.json`
5
+ * 의 button 항목을 그대로 반영한다.
6
+ */
7
+ export const BUTTON_VARIANTS = ["solid", "outlined", "text-button"] as const;
8
+ export const BUTTON_INTENTS = ["primary", "secondary", "teritary"] as const;
9
+ export const BUTTON_SIZES = ["xlarge", "large", "medium", "small"] as const;
10
+ export const BUTTON_SHAPES = ["default", "round"] as const;
11
+ export const BUTTON_STATES = ["default", "readonly", "disabled"] as const;
12
+ export const BUTTON_SIMULATED_STATES = ["hover", "pressed"] as const;
13
+
14
+ export type ButtonVariant = (typeof BUTTON_VARIANTS)[number];
15
+ export type ButtonIntent = (typeof BUTTON_INTENTS)[number];
16
+ export type ButtonSize = (typeof BUTTON_SIZES)[number];
17
+ export type ButtonShape = (typeof BUTTON_SHAPES)[number];
18
+ export type ButtonState = (typeof BUTTON_STATES)[number];
19
+ export type ButtonSimulatedState = (typeof BUTTON_SIMULATED_STATES)[number];
20
+ export type ButtonIconSlot = "none" | "left" | "right";
21
+
22
+ type NativeButtonProps = ComponentPropsWithoutRef<"button">;
23
+ export interface ButtonProps extends Omit<
24
+ NativeButtonProps,
25
+ "children" | "prefix" | "suffix"
26
+ > {
27
+ /**
28
+ * 주 버튼 내용. 문자열이면 `<span class="button-label">`로 자동 감싼다.
29
+ */
30
+ children?: ReactNode;
31
+ /**
32
+ * SOT에서 정의한 버튼 스타일 축(solid/outlined/text-button).
33
+ */
34
+ variant?: ButtonVariant;
35
+ /**
36
+ * UX priority(semantic color). foundation semantic 색과 매핑된다.
37
+ */
38
+ intent?: ButtonIntent;
39
+ /**
40
+ * 디자인 토큰 사이즈 alias(small~xlarge). Theme size 단계와 매핑된다.
41
+ */
42
+ size?: ButtonSize;
43
+ /**
44
+ * radius alias. round는 full radius를 사용한다.
45
+ */
46
+ shape?: ButtonShape;
47
+ /**
48
+ * UI state: readonly/disabled 를 명시적으로 설정할 수 있다.
49
+ */
50
+ state?: ButtonState;
51
+ /**
52
+ * block이면 width:100%로 확장된다.
53
+ */
54
+ block?: boolean;
55
+ /**
56
+ * Theme의 loading 속성과 연동된다.
57
+ */
58
+ loading?: boolean;
59
+ /**
60
+ * 아이콘 슬롯 위치. left/right/none.
61
+ */
62
+ iconSlot?: ButtonIconSlot;
63
+ /**
64
+ * 아이콘 노드. 슬롯과 함께 전달한다.
65
+ */
66
+ icon?: ReactNode;
67
+ /**
68
+ * 라벨 앞에 표시할 prefix 슬롯.
69
+ */
70
+ prefix?: ReactElement;
71
+ /**
72
+ * 라벨 뒤에 표시할 suffix 슬롯.
73
+ */
74
+ suffix?: ReactElement;
75
+ /**
76
+ * Storybook에서 hover/pressed 상태를 강제로 보여주기 위한 data attribute.
77
+ */
78
+ "data-simulated-state"?: ButtonSimulatedState;
79
+ }
@@ -0,0 +1,58 @@
1
+ import clsx from "clsx";
2
+ import type {
3
+ ButtonIconSlot,
4
+ ButtonIntent,
5
+ ButtonShape,
6
+ ButtonSize,
7
+ ButtonState,
8
+ ButtonVariant,
9
+ } from "../types";
10
+
11
+ type ButtonClassNameOptions = {
12
+ variant: ButtonVariant;
13
+ intent: ButtonIntent;
14
+ size: ButtonSize;
15
+ shape: ButtonShape;
16
+ block: boolean;
17
+ loading: boolean;
18
+ iconSlot: ButtonIconSlot;
19
+ iconOnly: boolean;
20
+ state: ButtonState;
21
+ className?: string;
22
+ };
23
+
24
+ const BUTTON_CLASSNAME = "button";
25
+
26
+ /**
27
+ * 버튼 클래스 조합 helper: variant/intent/size 별 modifier를 한 곳에서 관리한다.
28
+ */
29
+ const composeButtonClassName = ({
30
+ variant,
31
+ intent,
32
+ size,
33
+ shape,
34
+ block,
35
+ loading,
36
+ iconSlot,
37
+ iconOnly,
38
+ state,
39
+ className,
40
+ }: ButtonClassNameOptions) =>
41
+ clsx(
42
+ BUTTON_CLASSNAME,
43
+ `${BUTTON_CLASSNAME}--variant-${variant}`,
44
+ `${BUTTON_CLASSNAME}--intent-${intent}`,
45
+ `${BUTTON_CLASSNAME}--size-${size}`,
46
+ `${BUTTON_CLASSNAME}--shape-${shape}`,
47
+ {
48
+ [`${BUTTON_CLASSNAME}--state-${state}`]: state !== "default",
49
+ [`${BUTTON_CLASSNAME}--block`]: block,
50
+ [`${BUTTON_CLASSNAME}--loading`]: loading,
51
+ [`${BUTTON_CLASSNAME}--icon-${iconSlot}`]: iconSlot !== "none",
52
+ [`${BUTTON_CLASSNAME}--icon-only`]: iconOnly,
53
+ },
54
+ className,
55
+ );
56
+
57
+ export { BUTTON_CLASSNAME, composeButtonClassName };
58
+ export type { ButtonClassNameOptions };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * TODO(calendar): 접근성/상태 계산 hook을 정의한다.
3
+ */
4
+ export {};
File without changes
@@ -0,0 +1 @@
1
+ @use "./styles/index.scss";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * calendar 카테고리 배럴 placeholder: 실제 구현은 markup/ 하위에 추가한다.
3
+ */
4
+ export * from "./markup";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * TODO(calendar): SOT 및 사용자 제약에 따라 컴포넌트를 구현한다.
3
+ */
4
+ export {};
@@ -0,0 +1,3 @@
1
+ @use "@uniai-fe/uds-foundation/css";
2
+
3
+ /* TODO(calendar): 스타일을 SOT 토큰 값으로 정의한다. */
@@ -0,0 +1,4 @@
1
+ /**
2
+ * TODO(calendar): variant/slot 타입 정의를 작성한다.
3
+ */
4
+ export {};
@@ -0,0 +1,4 @@
1
+ /**
2
+ * TODO(calendar): 토큰 매핑과 클래스명 유틸을 구현한다.
3
+ */
4
+ export {};
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Checkbox 전용 hook placeholder.
3
+ */
4
+ export {};
File without changes
@@ -0,0 +1,3 @@
1
+ <svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M0.699997 3.52844L4.23553 7.06398L10.5995 0.700012" stroke="white" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M0.170857 3.35307C-0.0569492 3.12526 -0.0569492 2.75569 0.170857 2.52788C0.398662 2.30008 0.768239 2.30008 0.996045 2.52788L3.52973 5.06157L8.42044 0.170859C8.64824 -0.0569466 9.01782 -0.056947 9.24562 0.170859C9.47343 0.398665 9.47343 0.768242 9.24562 0.996047L3.94232 6.29935C3.83293 6.40874 3.68444 6.47025 3.52973 6.47025C3.37502 6.47025 3.22653 6.40874 3.11713 6.29935L0.170857 3.35307Z" fill="white"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M0.170857 3.35307C-0.0569492 3.12526 -0.0569492 2.75569 0.170857 2.52788C0.398662 2.30008 0.768239 2.30008 0.996045 2.52788L3.52973 5.06157L8.42044 0.170859C8.64824 -0.0569466 9.01782 -0.056947 9.24562 0.170859C9.47343 0.398665 9.47343 0.768242 9.24562 0.996047L3.94232 6.29935C3.83293 6.40874 3.68444 6.47025 3.52973 6.47025C3.37502 6.47025 3.22653 6.40874 3.11713 6.29935L0.170857 3.35307Z" fill="white"/>
3
+ </svg>
@@ -0,0 +1 @@
1
+ @use "./styles/index.scss";
@@ -0,0 +1,4 @@
1
+ import "./index.scss";
2
+
3
+ export * from "./markup";
4
+ export type * from "./types";
@@ -0,0 +1,127 @@
1
+ import clsx from "clsx";
2
+ import { forwardRef, useId } from "react";
3
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
4
+ import type { CheckboxFieldProps, CheckboxProps, CheckboxSize } from "../types";
5
+ import CheckLargeIcon from "../img/check-large.svg";
6
+ import CheckMediumIcon from "../img/check-medium.svg";
7
+
8
+ const CHECKBOX_CLASSNAME = "checkbox";
9
+ const CHECKBOX_INDICATOR_CLASSNAME = "checkbox-indicator";
10
+ const CHECKBOX_FIELD_CLASSNAME = "checkbox-field";
11
+ const CHECKBOX_LABEL_WRAPPER_CLASSNAME = "checkbox-label-wrapper";
12
+ const CHECKBOX_LABEL_TEXT_CLASSNAME = "checkbox-label-text";
13
+ const CHECKBOX_HELPER_CLASSNAME = "checkbox-helper";
14
+
15
+ const getIndicatorIcon = (size: CheckboxSize) =>
16
+ size === "large" ? CheckLargeIcon : CheckMediumIcon;
17
+
18
+ /**
19
+ * 체크박스 컴포넌트; Radix Checkbox thin wrapper
20
+ * @component
21
+ * @param {CheckboxProps} props
22
+ * @param {CheckboxSize} [props.size] medium, large
23
+ * @example
24
+ * <Checkbox size="medium" checked />
25
+ */
26
+ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
27
+ function Checkbox(
28
+ { size = "medium", className, disabled, ...restProps },
29
+ ref,
30
+ ) {
31
+ const IndicatorIcon = getIndicatorIcon(size);
32
+
33
+ return (
34
+ <CheckboxPrimitive.Root
35
+ ref={ref}
36
+ disabled={disabled}
37
+ data-size={size}
38
+ data-disabled={disabled ? "true" : undefined}
39
+ className={clsx(CHECKBOX_CLASSNAME, className)}
40
+ {...restProps}
41
+ >
42
+ <CheckboxPrimitive.Indicator className={CHECKBOX_INDICATOR_CLASSNAME}>
43
+ <IndicatorIcon aria-hidden />
44
+ </CheckboxPrimitive.Indicator>
45
+ </CheckboxPrimitive.Root>
46
+ );
47
+ },
48
+ );
49
+
50
+ /**
51
+ * 체크박스 필드 컴포넌트; label/helper 텍스트 래퍼
52
+ * @component
53
+ * @param {CheckboxFieldProps} props
54
+ * @param {CheckboxSize} [props.size] medium, large
55
+ * @example
56
+ * <CheckboxField label="약관 동의" helperText="필수" checked />
57
+ */
58
+ export const CheckboxField = forwardRef<HTMLButtonElement, CheckboxFieldProps>(
59
+ function CheckboxField(
60
+ {
61
+ label,
62
+ helperText,
63
+ helperTextProps,
64
+ labelProps,
65
+ fieldClassName,
66
+ labelWrapperClassName,
67
+ size = "medium",
68
+ id,
69
+ disabled,
70
+ ...restProps
71
+ },
72
+ ref,
73
+ ) {
74
+ const generatedId = useId();
75
+ const checkboxId = id ?? generatedId;
76
+ const labelId = label ? `${checkboxId}-label` : undefined;
77
+ const helperId = helperText ? `${checkboxId}-helper` : undefined;
78
+ // Label/helper는 Root 외부에서 htmlFor/aria 연결을 유지한다.
79
+
80
+ return (
81
+ <div
82
+ className={clsx(CHECKBOX_FIELD_CLASSNAME, fieldClassName)}
83
+ data-size={size}
84
+ data-disabled={disabled ? "true" : undefined}
85
+ >
86
+ <label
87
+ htmlFor={checkboxId}
88
+ {...labelProps}
89
+ className={clsx(
90
+ CHECKBOX_LABEL_WRAPPER_CLASSNAME,
91
+ labelWrapperClassName,
92
+ labelProps?.className,
93
+ )}
94
+ data-disabled={disabled ? "true" : undefined}
95
+ aria-disabled={disabled ? "true" : undefined}
96
+ >
97
+ <Checkbox
98
+ {...restProps}
99
+ id={checkboxId}
100
+ size={size}
101
+ disabled={disabled}
102
+ aria-labelledby={labelId}
103
+ aria-describedby={helperId}
104
+ ref={ref}
105
+ />
106
+ {label ? (
107
+ <span id={labelId} className={CHECKBOX_LABEL_TEXT_CLASSNAME}>
108
+ {label}
109
+ </span>
110
+ ) : null}
111
+ </label>
112
+ {helperText ? (
113
+ <p
114
+ id={helperId}
115
+ {...helperTextProps}
116
+ className={clsx(
117
+ CHECKBOX_HELPER_CLASSNAME,
118
+ helperTextProps?.className,
119
+ )}
120
+ >
121
+ {helperText}
122
+ </p>
123
+ ) : null}
124
+ </div>
125
+ );
126
+ },
127
+ );
@@ -0,0 +1 @@
1
+ export { Checkbox, CheckboxField } from "./Checkbox";
@@ -0,0 +1,164 @@
1
+ @use "@uniai-fe/uds-foundation/css";
2
+
3
+ :where(.radix-themes, .theme-root, :root) {
4
+ --theme-checkbox-frame-size-medium: 20px;
5
+ --theme-checkbox-frame-size-large: 24px;
6
+ --theme-checkbox-indicator-size-medium: 16px;
7
+ --theme-checkbox-indicator-size-large: 20px;
8
+ --theme-checkbox-control-radius-medium: var(--theme-radius-xsmall-3, 3px);
9
+ --theme-checkbox-control-radius-large: var(--theme-radius-small-1, 4px);
10
+ --theme-checkbox-border-width: 1.4px;
11
+ --theme-checkbox-border-color: var(--color-border-standard-assistive);
12
+ --theme-checkbox-border-selected: var(--color-primary-default);
13
+ --theme-checkbox-surface: var(--color-common-100);
14
+ --theme-checkbox-surface-selected: var(--color-primary-default);
15
+ --theme-checkbox-surface-disabled: var(--color-neutral-95);
16
+ --theme-checkbox-label-color: var(--color-label-strong);
17
+ --theme-checkbox-label-disabled: var(--color-label-disabled);
18
+ --theme-checkbox-helper-color: var(--color-label-neutral);
19
+ --theme-checkbox-helper-disabled: var(--color-label-disabled);
20
+ --theme-checkbox-icon-color: var(--color-common-100);
21
+ --theme-checkbox-focus-ring: rgba(2, 84, 255, 0.32);
22
+ --theme-checkbox-disabled-selected-opacity: 0.28;
23
+ }
24
+
25
+ .checkbox {
26
+ display: inline-flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ inline-size: var(--theme-checkbox-frame-size-medium);
30
+ block-size: var(--theme-checkbox-frame-size-medium);
31
+ border-radius: var(--theme-checkbox-control-radius-medium);
32
+ background-color: transparent; // 프레임은 완전 투명
33
+ border: none;
34
+ cursor: pointer;
35
+ transition:
36
+ background-color 0.15s ease,
37
+ border-color 0.15s ease,
38
+ opacity 0.15s ease,
39
+ box-shadow 0.15s ease;
40
+
41
+ &[data-size="medium"] {
42
+ inline-size: var(--theme-checkbox-frame-size-medium);
43
+ block-size: var(--theme-checkbox-frame-size-medium);
44
+ border-radius: var(--theme-checkbox-control-radius-medium);
45
+ }
46
+
47
+ &[data-size="large"] {
48
+ inline-size: var(--theme-checkbox-frame-size-large);
49
+ block-size: var(--theme-checkbox-frame-size-large);
50
+ border-radius: var(--theme-checkbox-control-radius-large);
51
+ }
52
+
53
+ &[data-disabled="true"] {
54
+ opacity: 0.6;
55
+ cursor: not-allowed;
56
+ }
57
+ }
58
+
59
+ .checkbox:focus-visible {
60
+ box-shadow: 0 0 0 2px var(--theme-checkbox-focus-ring);
61
+ }
62
+
63
+ .checkbox-indicator {
64
+ inline-size: var(--theme-checkbox-indicator-size-medium);
65
+ block-size: var(--theme-checkbox-indicator-size-medium);
66
+ display: inline-flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ color: var(--theme-checkbox-icon-color);
70
+ border: var(--theme-checkbox-border-width) solid
71
+ var(--theme-checkbox-border-color);
72
+ border-radius: var(--theme-checkbox-control-radius-medium);
73
+ background-color: var(--theme-checkbox-surface);
74
+ transition:
75
+ background-color 0.15s ease,
76
+ border-color 0.15s ease,
77
+ color 0.15s ease,
78
+ opacity 0.15s ease;
79
+
80
+ svg {
81
+ display: block;
82
+ inline-size: auto;
83
+ block-size: auto;
84
+ max-inline-size: 100%;
85
+ max-block-size: 100%;
86
+ }
87
+ }
88
+
89
+ .checkbox[data-size="large"] .checkbox-indicator {
90
+ inline-size: var(--theme-checkbox-indicator-size-large);
91
+ block-size: var(--theme-checkbox-indicator-size-large);
92
+ border-radius: var(--theme-checkbox-control-radius-large);
93
+ }
94
+
95
+ // 인디케이터 영역(16x16 / 20x20)에만 상태 색상을 입혀 Figma 레이아웃과 동일하게 유지한다.
96
+ .checkbox[data-state="checked"] .checkbox-indicator,
97
+ .checkbox[data-state="indeterminate"] .checkbox-indicator {
98
+ background-color: var(--theme-checkbox-surface-selected);
99
+ border-color: var(--theme-checkbox-border-selected);
100
+ }
101
+
102
+ .checkbox[data-disabled="true"] .checkbox-indicator {
103
+ background-color: var(--theme-checkbox-surface-disabled);
104
+ border-color: var(--theme-checkbox-border-color);
105
+ opacity: 1;
106
+ }
107
+
108
+ .checkbox[data-disabled="true"][data-state="checked"] .checkbox-indicator,
109
+ .checkbox[data-disabled="true"][data-state="indeterminate"]
110
+ .checkbox-indicator {
111
+ // Figma 기준 disabled selected 상태는 primary 면을 유지하며 투명도로 구분한다.
112
+ background-color: var(--theme-checkbox-surface-selected);
113
+ border-color: var(--theme-checkbox-border-selected);
114
+ opacity: var(--theme-checkbox-disabled-selected-opacity);
115
+ }
116
+
117
+ .checkbox-field {
118
+ display: flex;
119
+ flex-direction: column;
120
+ gap: var(--spacing-gap-1);
121
+ color: var(--theme-checkbox-label-color);
122
+ }
123
+
124
+ .checkbox-field[data-disabled="true"] {
125
+ color: var(--theme-checkbox-label-disabled);
126
+ }
127
+
128
+ .checkbox-label-wrapper {
129
+ display: inline-flex;
130
+ align-items: center;
131
+ gap: var(--spacing-gap-2);
132
+ cursor: pointer;
133
+ }
134
+
135
+ .checkbox-label-wrapper[data-disabled="true"] {
136
+ cursor: not-allowed;
137
+ }
138
+
139
+ .checkbox-label-text {
140
+ font-weight: var(--font-body-medium-weight);
141
+ user-select: none;
142
+ }
143
+
144
+ .checkbox-field[data-size="medium"] .checkbox-label-text {
145
+ font-size: var(--font-body-xsmall-size);
146
+ line-height: var(--font-body-xsmall-line-height);
147
+ }
148
+
149
+ .checkbox-field[data-size="large"] .checkbox-label-text {
150
+ font-size: var(--font-body-medium-size);
151
+ line-height: var(--font-body-medium-line-height);
152
+ }
153
+
154
+ .checkbox-helper {
155
+ margin: 0;
156
+ font-size: var(--font-caption-large-size);
157
+ line-height: var(--font-caption-large-line-height);
158
+ color: var(--theme-checkbox-helper-color);
159
+ user-select: none;
160
+ }
161
+
162
+ .checkbox-field[data-disabled="true"] .checkbox-helper {
163
+ color: var(--theme-checkbox-helper-disabled);
164
+ }
@@ -0,0 +1,21 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import type * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
+
4
+ export type CheckboxSize = "medium" | "large";
5
+
6
+ export type CheckboxPrimitiveProps = ComponentPropsWithoutRef<
7
+ typeof CheckboxPrimitive.Root
8
+ >;
9
+
10
+ export interface CheckboxProps extends CheckboxPrimitiveProps {
11
+ size?: CheckboxSize;
12
+ }
13
+
14
+ export interface CheckboxFieldProps extends CheckboxProps {
15
+ label?: ReactNode;
16
+ helperText?: ReactNode;
17
+ helperTextProps?: ComponentPropsWithoutRef<"p">;
18
+ labelProps?: ComponentPropsWithoutRef<"label">;
19
+ fieldClassName?: string;
20
+ labelWrapperClassName?: string;
21
+ }
@@ -0,0 +1 @@
1
+ export type * from "./checkbox";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * TODO(chips): 접근성/상태 계산 hook을 정의한다.
3
+ */
4
+ export {};
File without changes
@@ -0,0 +1,3 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M10.8889 4.28931C11.0972 4.08103 11.4355 4.08103 11.6438 4.28931C11.8521 4.49759 11.8521 4.83592 11.6438 5.04419L8.72095 7.96606L11.6438 10.8889C11.8521 11.0972 11.8521 11.4355 11.6438 11.6438C11.4355 11.8521 11.0972 11.8521 10.8889 11.6438L7.96606 8.72095L5.04419 11.6438C4.83592 11.8521 4.49759 11.8521 4.28931 11.6438C4.08103 11.4355 4.08103 11.0972 4.28931 10.8889L7.21118 7.96606L4.28931 5.04419C4.08103 4.83591 4.08103 4.49759 4.28931 4.28931C4.49759 4.08103 4.83591 4.08103 5.04419 4.28931L7.96606 7.21118L10.8889 4.28931Z" fill="currentColor"/>
3
+ </svg>
@@ -0,0 +1 @@
1
+ @use "./styles/index.scss";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * chips 카테고리 배럴 placeholder: 실제 구현은 markup/ 하위에 추가한다.
3
+ */
4
+ import "./index.scss";
5
+
6
+ export * from "./markup";
@@ -0,0 +1,103 @@
1
+ import { forwardRef, type Ref } from "react";
2
+ import RemoveIcon from "../img/remove.svg";
3
+ 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";
10
+
11
+ // kind === "input" 조합에서 props를 ChipInputProps로 좁히기 위한 type guard.
12
+ const isChipInputProps = (props: ChipProps): props is ChipInputProps =>
13
+ props.kind === "input";
14
+
15
+ /**
16
+ * Chip 종류별로 interactive/input 구조가 달라 별도 분기한다.
17
+ */
18
+ const Chip = forwardRef<HTMLElement, ChipProps>((props, ref) => {
19
+ if (isChipInputProps(props)) {
20
+ // input kind는 figure + remove 버튼 조합으로 구성한다.
21
+ const {
22
+ children,
23
+ className,
24
+ removeButtonLabel = "선택 항목 삭제",
25
+ onRemove,
26
+ selected,
27
+ leading,
28
+ ...restProps
29
+ } = props;
30
+ const hasLeading = Boolean(leading);
31
+ const combinedClassName = composeChipClassName({
32
+ kind: "input",
33
+ selected,
34
+ hasLeading,
35
+ removable: true,
36
+ className,
37
+ });
38
+
39
+ return (
40
+ <figure
41
+ {...restProps}
42
+ ref={ref as Ref<HTMLElementTagNameMap["figure"]>}
43
+ className={combinedClassName}
44
+ data-kind="input"
45
+ data-removable="true"
46
+ data-selected={selected ? "true" : undefined}
47
+ data-has-leading={hasLeading ? "true" : undefined}
48
+ >
49
+ <span className={CHIP_LABEL_CLASSNAME}>{children}</span>
50
+ <button
51
+ type="button"
52
+ className={CHIP_REMOVE_BUTTON_CLASSNAME}
53
+ aria-label={removeButtonLabel}
54
+ onClick={onRemove}
55
+ >
56
+ <RemoveIcon aria-hidden="true" />
57
+ </button>
58
+ </figure>
59
+ );
60
+ }
61
+
62
+ const { children, selected, leading, className, kind, type, ...restProps } =
63
+ props;
64
+ const resolvedKind = kind ?? "filter";
65
+ const isAssist = resolvedKind === "assist";
66
+ const hasLeading = isAssist && Boolean(leading);
67
+ const combinedClassName = composeChipClassName({
68
+ kind: resolvedKind,
69
+ selected,
70
+ hasLeading,
71
+ removable: false,
72
+ className,
73
+ });
74
+
75
+ return (
76
+ <button
77
+ {...restProps}
78
+ ref={ref as Ref<HTMLButtonElement>}
79
+ type={type ?? "button"}
80
+ className={combinedClassName}
81
+ data-kind={resolvedKind}
82
+ data-selected={selected ? "true" : undefined}
83
+ data-has-leading={hasLeading ? "true" : undefined}
84
+ aria-pressed={
85
+ typeof selected === "boolean" &&
86
+ (resolvedKind === "filter" || resolvedKind === "filter-rounded")
87
+ ? selected
88
+ : undefined
89
+ }
90
+ >
91
+ {isAssist && leading ? (
92
+ <span className={CHIP_LEADING_CLASSNAME} aria-hidden="true">
93
+ {leading}
94
+ </span>
95
+ ) : null}
96
+ <span className={CHIP_LABEL_CLASSNAME}>{children}</span>
97
+ </button>
98
+ );
99
+ });
100
+
101
+ Chip.displayName = "Chip";
102
+
103
+ export { Chip };
@@ -0,0 +1 @@
1
+ export { Chip } from "./Chip";