@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,54 @@
1
+ import clsx from "clsx";
2
+ import type { InputClassNameOptions } from "../types";
3
+
4
+ const INPUT_CLASSNAME = "input";
5
+ const INPUT_BOX_CLASSNAME = "input-box";
6
+ const INPUT_FIELD_CLASSNAME = "input-field";
7
+ const INPUT_ELEMENT_CLASSNAME = "input-element";
8
+ const INPUT_AFFIX_CLASSNAME = "input-affix";
9
+
10
+ const composeInputClassName = ({
11
+ appearance,
12
+ size,
13
+ state,
14
+ block,
15
+ className,
16
+ }: InputClassNameOptions) =>
17
+ clsx(
18
+ INPUT_CLASSNAME,
19
+ `${INPUT_CLASSNAME}--appearance-${appearance}`,
20
+ `${INPUT_CLASSNAME}--size-${size}`,
21
+ {
22
+ [`${INPUT_CLASSNAME}--state-${state}`]: state !== "default",
23
+ [`${INPUT_CLASSNAME}--block`]: block,
24
+ },
25
+ className,
26
+ );
27
+
28
+ const composeInputBoxClassName = ({
29
+ appearance,
30
+ size,
31
+ state,
32
+ block,
33
+ className,
34
+ }: InputClassNameOptions) =>
35
+ clsx(
36
+ INPUT_BOX_CLASSNAME,
37
+ `${INPUT_BOX_CLASSNAME}--appearance-${appearance}`,
38
+ `${INPUT_BOX_CLASSNAME}--size-${size}`,
39
+ {
40
+ [`${INPUT_BOX_CLASSNAME}--state-${state}`]: state !== "default",
41
+ [`${INPUT_BOX_CLASSNAME}--block`]: block,
42
+ },
43
+ className,
44
+ );
45
+
46
+ export {
47
+ INPUT_AFFIX_CLASSNAME,
48
+ INPUT_BOX_CLASSNAME,
49
+ INPUT_CLASSNAME,
50
+ INPUT_ELEMENT_CLASSNAME,
51
+ INPUT_FIELD_CLASSNAME,
52
+ composeInputBoxClassName,
53
+ composeInputClassName,
54
+ };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * TODO(label): 접근성/상태 계산 hook을 정의한다.
3
+ */
4
+ export {};
File without changes
@@ -0,0 +1 @@
1
+ @use "./styles/index.scss";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * label 카테고리 배럴 placeholder: 실제 구현은 markup/ 하위에 추가한다.
3
+ */
4
+ export * from "./markup";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * TODO(label): SOT 및 사용자 제약에 따라 컴포넌트를 구현한다.
3
+ */
4
+ export {};
@@ -0,0 +1,3 @@
1
+ @use "@uniai-fe/uds-foundation/css";
2
+
3
+ /* TODO(label): 스타일을 SOT 토큰 값으로 정의한다. */
@@ -0,0 +1,4 @@
1
+ /**
2
+ * TODO(label): variant/slot 타입 정의를 작성한다.
3
+ */
4
+ export {};
@@ -0,0 +1,4 @@
1
+ /**
2
+ * TODO(label): 토큰 매핑과 클래스명 유틸을 구현한다.
3
+ */
4
+ export {};
@@ -0,0 +1,4 @@
1
+ /**
2
+ * TODO(navigation): 접근성/상태 계산 hook을 정의한다.
3
+ */
4
+ export {};
File without changes
@@ -0,0 +1 @@
1
+ @use "./styles/index.scss";
@@ -0,0 +1,8 @@
1
+ import "./index.scss";
2
+
3
+ export * from "./markup";
4
+ export type {
5
+ BottomNavigationProps,
6
+ NavigationItem,
7
+ NavigationItemKey,
8
+ } from "./types";
@@ -0,0 +1,2 @@
1
+ export * from "./mobile";
2
+ export * from "./web";
@@ -0,0 +1,127 @@
1
+ import { forwardRef, type MouseEvent } from "react";
2
+ import type { BottomNavigationProps, NavigationItem } from "../../types";
3
+ import {
4
+ BOTTOM_NAVIGATION_ICON_CLASSNAME,
5
+ BOTTOM_NAVIGATION_ITEM_CLASSNAME,
6
+ BOTTOM_NAVIGATION_LABEL_CLASSNAME,
7
+ BOTTOM_NAVIGATION_LIST_CLASSNAME,
8
+ BOTTOM_NAVIGATION_TRIGGER_CLASSNAME,
9
+ composeNavigationClassName,
10
+ isHrefNavigationItem,
11
+ } from "../../utils";
12
+
13
+ const BottomNavigation = forwardRef<HTMLElement, BottomNavigationProps>(
14
+ (
15
+ {
16
+ items,
17
+ activeKey,
18
+ onActiveChange,
19
+ ariaLabel,
20
+ className,
21
+ fixed = false,
22
+ ...restProps
23
+ },
24
+ ref,
25
+ ) => {
26
+ if (!items || items.length === 0) {
27
+ return null;
28
+ }
29
+
30
+ const handleSelect = (item: NavigationItem) => {
31
+ if (item.disabled) {
32
+ return;
33
+ }
34
+
35
+ if (!isHrefNavigationItem(item)) {
36
+ item.onSelect(item.key);
37
+ }
38
+ onActiveChange?.(item.key);
39
+ };
40
+
41
+ const handleAnchorClick = (
42
+ event: MouseEvent<HTMLAnchorElement>,
43
+ item: NavigationItem,
44
+ ) => {
45
+ if (item.disabled) {
46
+ event.preventDefault();
47
+ event.stopPropagation();
48
+ return;
49
+ }
50
+ handleSelect(item);
51
+ };
52
+
53
+ const navClassName = composeNavigationClassName({ className });
54
+ const resolvedAriaLabel =
55
+ ariaLabel ?? (restProps as { ["aria-label"]?: string })["aria-label"];
56
+
57
+ return (
58
+ <nav
59
+ {...restProps}
60
+ ref={ref}
61
+ className={navClassName}
62
+ aria-label={resolvedAriaLabel}
63
+ data-fixed={fixed ? "true" : undefined}
64
+ >
65
+ <ul className={BOTTOM_NAVIGATION_LIST_CLASSNAME}>
66
+ {items.map(item => {
67
+ const isActive = item.key === activeKey;
68
+ const isDisabled = Boolean(item.disabled);
69
+ const sharedTriggerProps = {
70
+ className: BOTTOM_NAVIGATION_TRIGGER_CLASSNAME,
71
+ "data-active": isActive ? "true" : undefined,
72
+ "data-disabled": isDisabled ? "true" : undefined,
73
+ "aria-label": item.ariaLabel ?? item.label,
74
+ } as const;
75
+
76
+ return (
77
+ <li
78
+ key={item.key}
79
+ className={BOTTOM_NAVIGATION_ITEM_CLASSNAME}
80
+ data-active={isActive ? "true" : undefined}
81
+ data-disabled={isDisabled ? "true" : undefined}
82
+ >
83
+ {isHrefNavigationItem(item) ? (
84
+ // href 아이템은 anchor로 유지해 라우팅 흐름을 깨지 않는다.
85
+ <a
86
+ {...sharedTriggerProps}
87
+ href={item.href}
88
+ aria-current={isActive ? "page" : undefined}
89
+ aria-disabled={isDisabled ? "true" : undefined}
90
+ tabIndex={isDisabled ? -1 : undefined}
91
+ onClick={event => handleAnchorClick(event, item)}
92
+ >
93
+ <span className={BOTTOM_NAVIGATION_ICON_CLASSNAME}>
94
+ {item.icon}
95
+ </span>
96
+ <span className={BOTTOM_NAVIGATION_LABEL_CLASSNAME}>
97
+ {item.label}
98
+ </span>
99
+ </a>
100
+ ) : (
101
+ <button
102
+ {...sharedTriggerProps}
103
+ type="button"
104
+ aria-pressed={isActive ? true : undefined}
105
+ disabled={isDisabled}
106
+ onClick={() => handleSelect(item)}
107
+ >
108
+ <span className={BOTTOM_NAVIGATION_ICON_CLASSNAME}>
109
+ {item.icon}
110
+ </span>
111
+ <span className={BOTTOM_NAVIGATION_LABEL_CLASSNAME}>
112
+ {item.label}
113
+ </span>
114
+ </button>
115
+ )}
116
+ </li>
117
+ );
118
+ })}
119
+ </ul>
120
+ </nav>
121
+ );
122
+ },
123
+ );
124
+
125
+ BottomNavigation.displayName = "BottomNavigation";
126
+
127
+ export { BottomNavigation };
@@ -0,0 +1 @@
1
+ export { BottomNavigation } from "./BottomNavigation";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * TODO(navigation/web): 플랫폼 확정 후 구현을 추가한다.
3
+ */
4
+ export {};
@@ -0,0 +1,133 @@
1
+ @use "@uniai-fe/uds-foundation/css";
2
+
3
+ :where(.radix-themes, .theme-root, :root) {
4
+ --theme-navigation-height: 86px;
5
+ --theme-navigation-padding-inline: 32px;
6
+ --theme-navigation-padding-block-start: 8px;
7
+ --theme-navigation-padding-block-end: 20px;
8
+ --theme-navigation-item-gap: 2px;
9
+ --theme-navigation-icon-size: 24px;
10
+ --theme-navigation-label-font-size: 12px;
11
+ --theme-navigation-label-line-height: 1.4;
12
+ --theme-navigation-label-font-weight: 500;
13
+ --theme-navigation-label-letter-spacing: -0.12px;
14
+ --theme-navigation-bg: var(--color-common-100, #ffffff);
15
+ --theme-navigation-border: var(--color-border-standard-assistive, #e4e5e7);
16
+ --theme-navigation-shadow: 0 -4px 16px rgba(0, 0, 0, 0.04);
17
+ --theme-navigation-color: var(--color-label-strong, #3d3f43);
18
+ --theme-navigation-color-active: var(--color-primary-default, #0061ff);
19
+ --theme-navigation-color-disabled: var(--color-label-disabled, #c5c6cc);
20
+ }
21
+
22
+ .bottom-navigation {
23
+ width: 100%;
24
+ position: relative;
25
+ z-index: 10;
26
+ background-color: var(--theme-navigation-bg);
27
+ border-top: 1px solid var(--theme-navigation-border);
28
+ box-shadow: var(--theme-navigation-shadow, 0 -4px 16px rgba(0, 0, 0, 0.04));
29
+ padding-inline: var(--theme-navigation-padding-inline);
30
+ padding-block-start: var(--theme-navigation-padding-block-start);
31
+ padding-block-end: max(
32
+ var(--theme-navigation-padding-block-end),
33
+ env(safe-area-inset-bottom, 0px)
34
+ );
35
+ min-height: var(--theme-navigation-height);
36
+ }
37
+
38
+ .bottom-navigation[data-fixed="true"] {
39
+ position: fixed;
40
+ inset-inline: 0;
41
+ bottom: 0;
42
+ z-index: 20;
43
+ }
44
+
45
+ .bottom-navigation-list {
46
+ display: flex;
47
+ align-items: flex-start;
48
+ justify-content: space-between;
49
+ gap: 0;
50
+ padding: 0;
51
+ margin: 0;
52
+ list-style: none;
53
+ }
54
+
55
+ .bottom-navigation-item {
56
+ flex: 1;
57
+ min-width: 0;
58
+ display: flex;
59
+ justify-content: center;
60
+ }
61
+
62
+ .bottom-navigation-trigger {
63
+ display: inline-flex;
64
+ flex-direction: column;
65
+ align-items: center;
66
+ gap: var(--theme-navigation-item-gap);
67
+ min-width: 0;
68
+ padding: 0;
69
+ border: none;
70
+ background: none;
71
+ appearance: none;
72
+ text-decoration: none;
73
+ color: var(--theme-navigation-color);
74
+ font: inherit;
75
+ cursor: pointer;
76
+ -webkit-tap-highlight-color: transparent;
77
+ transition: color 0.16s ease;
78
+ }
79
+
80
+ .bottom-navigation-trigger[data-active="true"] {
81
+ color: var(--theme-navigation-color-active);
82
+ }
83
+
84
+ .bottom-navigation-trigger[data-disabled="true"],
85
+ .bottom-navigation-trigger:disabled {
86
+ color: var(--theme-navigation-color-disabled);
87
+ cursor: default;
88
+ pointer-events: none;
89
+ }
90
+
91
+ .bottom-navigation-trigger:focus-visible {
92
+ outline: none;
93
+ border-radius: var(--theme-radius-large-1, 12px);
94
+ box-shadow: 0 0 0 2px var(--color-primary-focus, rgba(0, 97, 255, 0.2));
95
+ }
96
+
97
+ .navigation-item-icon {
98
+ display: inline-flex;
99
+ align-items: center;
100
+ justify-content: center;
101
+ width: var(--theme-navigation-icon-size);
102
+ height: var(--theme-navigation-icon-size);
103
+ color: currentColor;
104
+
105
+ svg {
106
+ display: block;
107
+ width: 100%;
108
+ height: 100%;
109
+ }
110
+
111
+ /* SVG fill/stroke를 currentColor로 맞춰 selected 컬러만 제어한다. */
112
+ svg [fill]:not([fill="none"]) {
113
+ fill: currentColor;
114
+ }
115
+
116
+ svg [stroke]:not([stroke="none"]) {
117
+ stroke: currentColor;
118
+ }
119
+ }
120
+
121
+ .navigation-item-label {
122
+ display: block;
123
+ font-size: var(--theme-navigation-label-font-size);
124
+ font-weight: var(--theme-navigation-label-font-weight);
125
+ line-height: var(--theme-navigation-label-line-height);
126
+ letter-spacing: var(--theme-navigation-label-letter-spacing);
127
+ color: currentColor;
128
+ text-align: center;
129
+ white-space: nowrap;
130
+ overflow: hidden;
131
+ text-overflow: ellipsis;
132
+ max-width: 100%;
133
+ }
@@ -0,0 +1,38 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+
3
+ export type NavigationItemKey = string;
4
+
5
+ interface NavigationItemBase {
6
+ key: NavigationItemKey;
7
+ label: string;
8
+ icon: ReactNode;
9
+ disabled?: boolean;
10
+ ariaLabel?: string;
11
+ }
12
+
13
+ export type NavigationHrefItem = NavigationItemBase & {
14
+ href: string;
15
+ onSelect?: never;
16
+ };
17
+
18
+ export type NavigationActionItem = NavigationItemBase & {
19
+ onSelect: (key: NavigationItemKey) => void;
20
+ href?: never;
21
+ };
22
+
23
+ export type NavigationItem = NavigationHrefItem | NavigationActionItem;
24
+
25
+ export interface BottomNavigationProps extends Omit<
26
+ ComponentPropsWithoutRef<"nav">,
27
+ "children"
28
+ > {
29
+ items: NavigationItem[];
30
+ activeKey: NavigationItemKey | null;
31
+ onActiveChange?: (key: NavigationItemKey) => void;
32
+ ariaLabel?: string;
33
+ fixed?: boolean;
34
+ }
35
+
36
+ export interface NavigationClassNameOptions {
37
+ className?: string;
38
+ }
@@ -0,0 +1,23 @@
1
+ import clsx from "clsx";
2
+ import type {
3
+ NavigationClassNameOptions,
4
+ NavigationHrefItem,
5
+ NavigationItem,
6
+ } from "../types";
7
+
8
+ export const BOTTOM_NAVIGATION_CLASSNAME = "bottom-navigation";
9
+ export const BOTTOM_NAVIGATION_LIST_CLASSNAME = `${BOTTOM_NAVIGATION_CLASSNAME}-list`;
10
+ export const BOTTOM_NAVIGATION_ITEM_CLASSNAME = `${BOTTOM_NAVIGATION_CLASSNAME}-item`;
11
+ export const BOTTOM_NAVIGATION_TRIGGER_CLASSNAME = `${BOTTOM_NAVIGATION_CLASSNAME}-trigger`;
12
+ export const BOTTOM_NAVIGATION_ICON_CLASSNAME = "navigation-item-icon";
13
+ export const BOTTOM_NAVIGATION_LABEL_CLASSNAME = "navigation-item-label";
14
+
15
+ export const composeNavigationClassName = ({
16
+ className,
17
+ }: NavigationClassNameOptions = {}) =>
18
+ clsx(BOTTOM_NAVIGATION_CLASSNAME, className);
19
+
20
+ export const isHrefNavigationItem = (
21
+ item: NavigationItem,
22
+ ): item is NavigationHrefItem =>
23
+ "href" in item && typeof item.href === "string";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * TODO(pagination): 접근성/상태 계산 hook을 정의한다.
3
+ */
4
+ export {};
File without changes
@@ -0,0 +1 @@
1
+ @use "./styles/index.scss";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * pagination 카테고리 배럴 placeholder: 실제 구현은 markup/ 하위에 추가한다.
3
+ */
4
+ import "./index.scss";
5
+
6
+ export * from "./markup";
@@ -0,0 +1,76 @@
1
+ import { forwardRef } from "react";
2
+ import type { PaginationCarouselProps } from "../types";
3
+ import {
4
+ PAGINATION_BUTTON_CLASSNAME,
5
+ PAGINATION_ITEM_CLASSNAME,
6
+ composePaginationClassName,
7
+ createPaginationPages,
8
+ normalizePaginationState,
9
+ } from "../utils";
10
+
11
+ /**
12
+ * Carousel indicator Pagination. 점 형태로 현재 위치만 표시한다.
13
+ * @component
14
+ * @param {PaginationCarouselProps} props
15
+ * @param {number} props.total 전체 step 수.
16
+ * @param {number} [props.current=1] 현재 step index(1-indexed).
17
+ * @param {(page: number) => void} [props.onPageChange]
18
+ * 전달 시 dot를 클릭해 이동할 수 있고, 생략하면 모든 dot가 disabled 된다.
19
+ */
20
+ const PaginationCarousel = forwardRef<
21
+ HTMLUListElement,
22
+ PaginationCarouselProps
23
+ >(({ total, current = 1, onPageChange, className, ...restProps }, ref) => {
24
+ const { total: normalizedTotal, current: normalizedCurrent } =
25
+ normalizePaginationState({ total, current });
26
+ const pages = createPaginationPages(normalizedTotal);
27
+ const allowInteraction = typeof onPageChange === "function";
28
+ const rootClassName = composePaginationClassName({
29
+ variant: "carousel",
30
+ className,
31
+ });
32
+
33
+ const handleClick = (page: number) => {
34
+ if (!allowInteraction || page === normalizedCurrent) {
35
+ return;
36
+ }
37
+ onPageChange?.(page);
38
+ };
39
+
40
+ return (
41
+ <ul
42
+ {...restProps}
43
+ ref={ref}
44
+ className={rootClassName}
45
+ data-variant="carousel"
46
+ data-interactive={allowInteraction ? "true" : "false"}
47
+ >
48
+ {pages.map(page => {
49
+ const isActive = page === normalizedCurrent;
50
+ return (
51
+ <li
52
+ key={page}
53
+ className={PAGINATION_ITEM_CLASSNAME}
54
+ data-active={isActive ? "true" : undefined}
55
+ >
56
+ <button
57
+ type="button"
58
+ className={PAGINATION_BUTTON_CLASSNAME}
59
+ data-active={isActive ? "true" : undefined}
60
+ aria-label={`Step ${page}`}
61
+ disabled={!allowInteraction}
62
+ tabIndex={allowInteraction ? 0 : -1}
63
+ onClick={allowInteraction ? () => handleClick(page) : undefined}
64
+ >
65
+ <span className="pagination-dot" aria-hidden="true" />
66
+ </button>
67
+ </li>
68
+ );
69
+ })}
70
+ </ul>
71
+ );
72
+ });
73
+
74
+ PaginationCarousel.displayName = "PaginationCarousel";
75
+
76
+ export { PaginationCarousel };
@@ -0,0 +1,54 @@
1
+ import { forwardRef } from "react";
2
+ import type { PaginationCountProps } from "../types";
3
+ import {
4
+ PAGINATION_BUTTON_CLASSNAME,
5
+ composePaginationClassName,
6
+ normalizePaginationState,
7
+ } from "../utils";
8
+
9
+ /**
10
+ * Count(step) indicator; `1/5` 형태로 현재 위치만 표시한다.
11
+ * @component
12
+ * @param {PaginationCountProps} props
13
+ * @param {number} props.total 전체 step 수.
14
+ * @param {number} [props.current=1] 현재 step position.
15
+ * @param {"small" | "xsmall"} [props.size="small"] 높이/타이포 크기 옵션.
16
+ */
17
+ const PaginationCount = forwardRef<HTMLDivElement, PaginationCountProps>(
18
+ ({ total, current = 1, size = "small", className, ...restProps }, ref) => {
19
+ const { total: normalizedTotal, current: normalizedCurrent } =
20
+ normalizePaginationState({ total, current });
21
+ const rootClassName = composePaginationClassName({
22
+ variant: "count",
23
+ countSize: size,
24
+ className,
25
+ });
26
+
27
+ return (
28
+ <div
29
+ {...restProps}
30
+ ref={ref}
31
+ className={rootClassName}
32
+ data-variant="count"
33
+ data-interactive="false"
34
+ >
35
+ <button
36
+ type="button"
37
+ className={PAGINATION_BUTTON_CLASSNAME}
38
+ disabled
39
+ aria-label={`Step ${normalizedCurrent} of ${normalizedTotal}`}
40
+ >
41
+ <span className="pagination-count-current">{normalizedCurrent}</span>
42
+ <span className="pagination-count-divider" aria-hidden="true">
43
+ /
44
+ </span>
45
+ <span className="pagination-count-total">{normalizedTotal}</span>
46
+ </button>
47
+ </div>
48
+ );
49
+ },
50
+ );
51
+
52
+ PaginationCount.displayName = "PaginationCount";
53
+
54
+ export { PaginationCount };
@@ -0,0 +1,83 @@
1
+ import { forwardRef } from "react";
2
+ import type { PaginationProps } from "../types";
3
+ import {
4
+ PAGINATION_BUTTON_CLASSNAME,
5
+ PAGINATION_ITEM_CLASSNAME,
6
+ composePaginationClassName,
7
+ createPaginationPages,
8
+ normalizePaginationState,
9
+ } from "../utils";
10
+
11
+ /**
12
+ * 숫자 페이지형 Pagination.
13
+ * @component
14
+ * @param {PaginationProps} props
15
+ * @param {number} props.total 전체 페이지 개수(1 이상으로 자동 보정).
16
+ * @param {number} [props.current=1] 현재 페이지. total 범위를 벗어나면 자동 조정된다.
17
+ * @param {(page: number) => void} [props.onPageChange]
18
+ * 페이지 버튼 클릭 핸들러. 전달되지 않으면 모든 버튼이 disabled 처리되어 cursor가 제거된다.
19
+ * @example
20
+ * ```tsx
21
+ * <Pagination total={10} current={3} onPageChange={setPage} />
22
+ * ```
23
+ */
24
+ const Pagination = forwardRef<HTMLUListElement, PaginationProps>(
25
+ ({ total, current = 1, onPageChange, className, ...restProps }, ref) => {
26
+ const { total: normalizedTotal, current: normalizedCurrent } =
27
+ normalizePaginationState({ total, current });
28
+ const pages = createPaginationPages(normalizedTotal);
29
+ const allowInteraction = typeof onPageChange === "function";
30
+ const rootClassName = composePaginationClassName({
31
+ variant: "list",
32
+ className,
33
+ });
34
+
35
+ const handleClick = (page: number) => {
36
+ if (!allowInteraction || page === normalizedCurrent) {
37
+ return;
38
+ }
39
+ onPageChange?.(page);
40
+ };
41
+
42
+ return (
43
+ <ul
44
+ {...restProps}
45
+ ref={ref}
46
+ className={rootClassName}
47
+ data-variant="list"
48
+ data-interactive={allowInteraction ? "true" : "false"}
49
+ >
50
+ {pages.map(page => {
51
+ const isActive = page === normalizedCurrent;
52
+ return (
53
+ <li
54
+ key={page}
55
+ className={PAGINATION_ITEM_CLASSNAME}
56
+ data-active={isActive ? "true" : undefined}
57
+ >
58
+ <button
59
+ type="button"
60
+ className={PAGINATION_BUTTON_CLASSNAME}
61
+ data-active={isActive ? "true" : undefined}
62
+ data-page={page}
63
+ aria-current={isActive ? "page" : undefined}
64
+ aria-label={`Page ${page}`}
65
+ disabled={!allowInteraction}
66
+ tabIndex={allowInteraction ? 0 : -1}
67
+ onClick={allowInteraction ? () => handleClick(page) : undefined}
68
+ >
69
+ <span className="pagination-number" data-value={page}>
70
+ {page}
71
+ </span>
72
+ </button>
73
+ </li>
74
+ );
75
+ })}
76
+ </ul>
77
+ );
78
+ },
79
+ );
80
+
81
+ Pagination.displayName = "Pagination";
82
+
83
+ export { Pagination };