@seed-design/lynx-react 0.1.1-alpha.0 → 0.2.0-alpha.2

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 (180) hide show
  1. package/lib/components/ActionButton/ActionButton.d.ts +40 -9
  2. package/lib/components/ActionButton/ActionButton.d.ts.map +1 -1
  3. package/lib/components/ActionButton/ActionButton.jsx +132 -14
  4. package/lib/components/BottomSheet/BottomSheet.d.ts +92 -0
  5. package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -0
  6. package/lib/components/BottomSheet/BottomSheet.jsx +178 -0
  7. package/lib/components/BottomSheet/BottomSheet.namespace.d.ts +2 -0
  8. package/lib/components/BottomSheet/BottomSheet.namespace.d.ts.map +1 -0
  9. package/lib/components/BottomSheet/BottomSheet.namespace.js +1 -0
  10. package/lib/components/BottomSheet/index.d.ts +3 -0
  11. package/lib/components/BottomSheet/index.d.ts.map +1 -0
  12. package/lib/components/BottomSheet/index.js +2 -0
  13. package/lib/components/Checkbox/Checkbox.d.ts +51 -0
  14. package/lib/components/Checkbox/Checkbox.d.ts.map +1 -0
  15. package/lib/components/Checkbox/Checkbox.jsx +121 -0
  16. package/lib/components/Checkbox/Checkbox.namespace.d.ts +2 -0
  17. package/lib/components/Checkbox/Checkbox.namespace.d.ts.map +1 -0
  18. package/lib/components/Checkbox/Checkbox.namespace.js +1 -0
  19. package/lib/components/Checkbox/index.d.ts +3 -0
  20. package/lib/components/Checkbox/index.d.ts.map +1 -0
  21. package/lib/components/Checkbox/index.js +2 -0
  22. package/lib/components/ProgressCircle/ProgressCircle.d.ts +10 -9
  23. package/lib/components/ProgressCircle/ProgressCircle.d.ts.map +1 -1
  24. package/lib/components/ProgressCircle/ProgressCircle.jsx +8 -14
  25. package/lib/components/ProgressCircle/ProgressCircle.namespace.d.ts +2 -0
  26. package/lib/components/ProgressCircle/ProgressCircle.namespace.d.ts.map +1 -0
  27. package/lib/components/ProgressCircle/ProgressCircle.namespace.js +1 -0
  28. package/lib/components/ProgressCircle/index.d.ts +2 -1
  29. package/lib/components/ProgressCircle/index.d.ts.map +1 -1
  30. package/lib/components/ProgressCircle/index.js +2 -1
  31. package/lib/components/RadioGroup/RadioGroup.d.ts +50 -0
  32. package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -0
  33. package/lib/components/RadioGroup/RadioGroup.jsx +144 -0
  34. package/lib/components/RadioGroup/RadioGroup.namespace.d.ts +2 -0
  35. package/lib/components/RadioGroup/RadioGroup.namespace.d.ts.map +1 -0
  36. package/lib/components/RadioGroup/RadioGroup.namespace.js +1 -0
  37. package/lib/components/RadioGroup/index.d.ts +3 -0
  38. package/lib/components/RadioGroup/index.d.ts.map +1 -0
  39. package/lib/components/RadioGroup/index.js +2 -0
  40. package/lib/components/Switch/Switch.d.ts +27 -0
  41. package/lib/components/Switch/Switch.d.ts.map +1 -0
  42. package/lib/components/Switch/Switch.jsx +75 -0
  43. package/lib/components/Switch/Switch.namespace.d.ts +2 -0
  44. package/lib/components/Switch/Switch.namespace.d.ts.map +1 -0
  45. package/lib/components/Switch/Switch.namespace.js +1 -0
  46. package/lib/components/Switch/index.d.ts +3 -0
  47. package/lib/components/Switch/index.d.ts.map +1 -0
  48. package/lib/components/Switch/index.js +2 -0
  49. package/lib/components/TagGroup/TagGroup.d.ts +40 -0
  50. package/lib/components/TagGroup/TagGroup.d.ts.map +1 -0
  51. package/lib/components/TagGroup/TagGroup.jsx +54 -0
  52. package/lib/components/TagGroup/TagGroup.namespace.d.ts +2 -0
  53. package/lib/components/TagGroup/TagGroup.namespace.d.ts.map +1 -0
  54. package/lib/components/TagGroup/TagGroup.namespace.js +1 -0
  55. package/lib/components/TagGroup/index.d.ts +3 -0
  56. package/lib/components/TagGroup/index.d.ts.map +1 -0
  57. package/lib/components/TagGroup/index.js +2 -0
  58. package/lib/components/index.d.ts +8 -0
  59. package/lib/components/index.d.ts.map +1 -0
  60. package/lib/components/index.js +7 -0
  61. package/lib/hooks/index.d.ts +4 -0
  62. package/lib/hooks/index.d.ts.map +1 -0
  63. package/lib/hooks/index.js +3 -0
  64. package/lib/hooks/use-controllable-state.d.ts +15 -0
  65. package/lib/hooks/use-controllable-state.d.ts.map +1 -0
  66. package/lib/hooks/use-controllable-state.js +27 -0
  67. package/lib/hooks/use-icon-color.d.ts +15 -0
  68. package/lib/hooks/use-icon-color.d.ts.map +1 -0
  69. package/lib/hooks/use-icon-color.js +37 -0
  70. package/lib/hooks/use-press-tap.d.ts +24 -0
  71. package/lib/hooks/use-press-tap.d.ts.map +1 -0
  72. package/lib/hooks/use-press-tap.js +35 -0
  73. package/lib/index.d.ts +3 -4
  74. package/lib/index.d.ts.map +1 -1
  75. package/lib/index.js +3 -4
  76. package/lib/utils/create-slot-recipe-context.d.ts +26 -0
  77. package/lib/utils/create-slot-recipe-context.d.ts.map +1 -0
  78. package/lib/utils/create-slot-recipe-context.jsx +76 -0
  79. package/lib/utils/get-seed-class-name.d.ts.map +1 -0
  80. package/lib/utils/index.d.ts +5 -0
  81. package/lib/utils/index.d.ts.map +1 -0
  82. package/lib/utils/index.js +4 -0
  83. package/lib/utils/resolve-recipe-token.d.ts +38 -0
  84. package/lib/utils/resolve-recipe-token.d.ts.map +1 -0
  85. package/lib/utils/resolve-recipe-token.js +47 -0
  86. package/lib/utils/split-multiple-variants-props.d.ts +29 -0
  87. package/lib/utils/split-multiple-variants-props.d.ts.map +1 -0
  88. package/lib/utils/split-multiple-variants-props.js +34 -0
  89. package/package.json +12 -9
  90. package/src/components/ActionButton/ActionButton.tsx +290 -31
  91. package/src/components/ActionButton/__tests__/ActionButton.test.tsx +35 -0
  92. package/src/components/BottomSheet/BottomSheet.namespace.ts +24 -0
  93. package/src/components/BottomSheet/BottomSheet.tsx +386 -0
  94. package/src/components/BottomSheet/__tests__/BottomSheet.test.tsx +119 -0
  95. package/src/components/BottomSheet/index.ts +27 -0
  96. package/src/components/Checkbox/Checkbox.namespace.ts +12 -0
  97. package/src/components/Checkbox/Checkbox.tsx +291 -0
  98. package/src/components/Checkbox/index.ts +14 -0
  99. package/src/components/ProgressCircle/ProgressCircle.namespace.ts +5 -0
  100. package/src/components/ProgressCircle/ProgressCircle.tsx +14 -13
  101. package/src/components/ProgressCircle/index.ts +9 -1
  102. package/src/components/RadioGroup/RadioGroup.namespace.ts +12 -0
  103. package/src/components/RadioGroup/RadioGroup.tsx +338 -0
  104. package/src/components/RadioGroup/index.ts +14 -0
  105. package/src/components/Switch/Switch.namespace.ts +10 -0
  106. package/src/components/Switch/Switch.tsx +191 -0
  107. package/src/components/Switch/index.ts +12 -0
  108. package/src/components/TagGroup/TagGroup.namespace.ts +8 -0
  109. package/src/components/TagGroup/TagGroup.tsx +125 -0
  110. package/src/components/TagGroup/index.ts +10 -0
  111. package/src/components/__tests__/ProgressCircle.test.ts +33 -0
  112. package/src/components/__tests__/exports.test.ts +38 -0
  113. package/src/components/__tests__/switch.test.ts +34 -0
  114. package/src/components/index.ts +7 -0
  115. package/src/hooks/__tests__/use-controllable-state.test.ts +120 -0
  116. package/src/hooks/__tests__/use-press-tap.test.ts +129 -0
  117. package/src/hooks/index.ts +3 -0
  118. package/src/hooks/use-controllable-state.ts +38 -0
  119. package/src/hooks/use-icon-color.ts +51 -0
  120. package/src/hooks/use-press-tap.ts +54 -0
  121. package/src/index.ts +3 -4
  122. package/src/utils/__tests__/create-slot-recipe-context.test.tsx +228 -0
  123. package/src/utils/__tests__/split-multiple-variants-props.test.ts +61 -0
  124. package/src/utils/create-slot-recipe-context.tsx +147 -0
  125. package/src/utils/index.ts +4 -0
  126. package/src/utils/resolve-recipe-token.ts +47 -0
  127. package/src/utils/split-multiple-variants-props.ts +59 -0
  128. package/lib/components/Box/Box.d.ts +0 -10
  129. package/lib/components/Box/Box.d.ts.map +0 -1
  130. package/lib/components/Box/Box.jsx +0 -19
  131. package/lib/components/Box/index.d.ts +0 -2
  132. package/lib/components/Box/index.d.ts.map +0 -1
  133. package/lib/components/Box/index.js +0 -1
  134. package/lib/components/Flex/Flex.d.ts +0 -33
  135. package/lib/components/Flex/Flex.d.ts.map +0 -1
  136. package/lib/components/Flex/Flex.jsx +0 -7
  137. package/lib/components/Flex/index.d.ts +0 -2
  138. package/lib/components/Flex/index.d.ts.map +0 -1
  139. package/lib/components/Flex/index.js +0 -1
  140. package/lib/components/List/List.d.ts +0 -9
  141. package/lib/components/List/List.d.ts.map +0 -1
  142. package/lib/components/List/List.jsx +0 -17
  143. package/lib/components/List/index.d.ts +0 -2
  144. package/lib/components/List/index.d.ts.map +0 -1
  145. package/lib/components/List/index.js +0 -1
  146. package/lib/components/ListItem/ListItem.d.ts +0 -8
  147. package/lib/components/ListItem/ListItem.d.ts.map +0 -1
  148. package/lib/components/ListItem/ListItem.jsx +0 -11
  149. package/lib/components/ListItem/index.d.ts +0 -2
  150. package/lib/components/ListItem/index.d.ts.map +0 -1
  151. package/lib/components/ListItem/index.js +0 -1
  152. package/lib/components/ScrollView/ScrollView.d.ts +0 -9
  153. package/lib/components/ScrollView/ScrollView.d.ts.map +0 -1
  154. package/lib/components/ScrollView/ScrollView.jsx +0 -17
  155. package/lib/components/ScrollView/index.d.ts +0 -2
  156. package/lib/components/ScrollView/index.d.ts.map +0 -1
  157. package/lib/components/ScrollView/index.js +0 -1
  158. package/lib/components/Stack/Stack.d.ts +0 -6
  159. package/lib/components/Stack/Stack.d.ts.map +0 -1
  160. package/lib/components/Stack/Stack.jsx +0 -10
  161. package/lib/components/Stack/index.d.ts +0 -2
  162. package/lib/components/Stack/index.d.ts.map +0 -1
  163. package/lib/components/Stack/index.js +0 -1
  164. package/lib/components/Text/Text.d.ts +0 -30
  165. package/lib/components/Text/Text.d.ts.map +0 -1
  166. package/lib/components/Text/Text.jsx +0 -49
  167. package/lib/components/Text/index.d.ts +0 -2
  168. package/lib/components/Text/index.d.ts.map +0 -1
  169. package/lib/components/Text/index.js +0 -1
  170. package/lib/get-seed-class-name.d.ts.map +0 -1
  171. package/lib/utils/dynamic-style.d.ts +0 -13
  172. package/lib/utils/dynamic-style.d.ts.map +0 -1
  173. package/lib/utils/dynamic-style.js +0 -26
  174. package/lib/utils/styled.d.ts +0 -120
  175. package/lib/utils/styled.d.ts.map +0 -1
  176. package/lib/utils/styled.jsx +0 -131
  177. package/src/utils/dynamic-style.ts +0 -26
  178. /package/lib/{get-seed-class-name.d.ts → utils/get-seed-class-name.d.ts} +0 -0
  179. /package/lib/{get-seed-class-name.js → utils/get-seed-class-name.js} +0 -0
  180. /package/src/{get-seed-class-name.ts → utils/get-seed-class-name.ts} +0 -0
@@ -0,0 +1,338 @@
1
+ import * as React from "react";
2
+ import { cloneElement, isValidElement, type CSSProperties, type ReactElement } from "react";
3
+ import clsx from "clsx";
4
+ import type { MainThread } from "@lynx-js/types";
5
+
6
+ import { radio } from "@seed-design/lynx-css/recipes/radio";
7
+ import type { RadioVariantProps } from "@seed-design/lynx-css/recipes/radio";
8
+ import { radiomark } from "@seed-design/lynx-css/recipes/radiomark";
9
+ import type { RadiomarkVariantProps } from "@seed-design/lynx-css/recipes/radiomark";
10
+ import { radioGroup } from "@seed-design/lynx-css/recipes/radio-group";
11
+ import { vars as radiomarkVars } from "@seed-design/lynx-css/vars/component/radiomark";
12
+
13
+ import { useControllableState } from "../../hooks/use-controllable-state";
14
+ import { useIconColor } from "../../hooks/use-icon-color";
15
+ import { usePressTap } from "../../hooks/use-press-tap";
16
+ import { capitalize, resolveRecipeToken } from "../../utils/resolve-recipe-token";
17
+ import { splitMultipleVariantsProps } from "../../utils/split-multiple-variants-props";
18
+
19
+ /**
20
+ * @platform Lynx
21
+ *
22
+ * 웹 대비 미지원 기능:
23
+ * - HiddenInput / name / required / invalid: Lynx에 native form 제출 모델이 없음
24
+ * - focus / focusVisible: Lynx에 키보드 포커스 개념이 없음
25
+ * - onChange (raw DOM event): 의미 없음. 선택 이벤트는 onValueChange로만 노출
26
+ *
27
+ * Indicator 는 `@karrotmarket/lynx-monochrome-icon` 의 monochrome icon 컴포넌트를
28
+ * 받는다. 내부에서 `<image tint-color=...>` 로 렌더되므로 `useIconColor` 훅이
29
+ * recipe 의 `color` 토큰을 `tint-color` 로 동기화한다.
30
+ */
31
+
32
+ type RadioSize = NonNullable<RadioVariantProps["size"]>;
33
+ type RadioWeight = NonNullable<RadioVariantProps["weight"]>;
34
+ type RadiomarkTone = NonNullable<RadiomarkVariantProps["tone"]>;
35
+
36
+ interface RadioGroupContextValue {
37
+ value: string | null;
38
+ setValue: (value: string) => void;
39
+ disabled: boolean;
40
+ size: RadioSize;
41
+ weight: RadioWeight;
42
+ tone: RadiomarkTone;
43
+ }
44
+
45
+ const RadioGroupContext = React.createContext<RadioGroupContextValue | null>(null);
46
+
47
+ function useRadioGroupContext(consumer: string): RadioGroupContextValue {
48
+ const ctx = React.useContext(RadioGroupContext);
49
+ if (!ctx) {
50
+ throw new Error(`<${consumer}/> must be rendered inside <RadioGroupRoot/>.`);
51
+ }
52
+ return ctx;
53
+ }
54
+
55
+ interface RadioGroupItemContextValue {
56
+ value: string;
57
+ checked: boolean;
58
+ disabled: boolean;
59
+ pressed: boolean;
60
+ tone: RadiomarkTone;
61
+ size: RadioSize;
62
+ }
63
+
64
+ const RadioGroupItemContext = React.createContext<RadioGroupItemContextValue | null>(null);
65
+
66
+ function useRadioGroupItemContext(consumer: string): RadioGroupItemContextValue {
67
+ const ctx = React.useContext(RadioGroupItemContext);
68
+ if (!ctx) {
69
+ throw new Error(`<${consumer}/> must be rendered inside <RadioGroupItem/>.`);
70
+ }
71
+ return ctx;
72
+ }
73
+
74
+ const RadiomarkIconClassContext = React.createContext<string | null>(null);
75
+
76
+ function useRadiomarkIconClass(consumer: string): string {
77
+ const ctx = React.useContext(RadiomarkIconClassContext);
78
+ if (!ctx) {
79
+ throw new Error(`<${consumer}/> must be rendered inside <RadioGroupItemControl/>.`);
80
+ }
81
+ return ctx;
82
+ }
83
+
84
+ ////////////////////////////////////////////////////////////////////////////////////
85
+
86
+ export interface RadioGroupRootProps
87
+ extends RadioVariantProps,
88
+ Omit<RadiomarkVariantProps, "size" | "checked" | "disabled"> {
89
+ children?: React.ReactNode;
90
+ className?: string;
91
+ value?: string;
92
+ defaultValue?: string;
93
+ disabled?: boolean;
94
+ onValueChange?: (value: string) => void;
95
+ }
96
+
97
+ export const RadioGroupRoot = React.forwardRef<unknown, RadioGroupRootProps>((props, ref) => {
98
+ const {
99
+ children,
100
+ className,
101
+ value: valueProp,
102
+ defaultValue,
103
+ disabled = false,
104
+ onValueChange,
105
+ ...restProps
106
+ } = props;
107
+ const [{ radio: radioVariantProps, radiomark: radiomarkVariantProps }, nativeProps] =
108
+ splitMultipleVariantsProps(restProps, { radio, radiomark });
109
+ const weight: RadioWeight = radioVariantProps.weight ?? "regular";
110
+ const size: RadioSize = radioVariantProps.size ?? "medium";
111
+ const tone: RadiomarkTone = radiomarkVariantProps.tone ?? "brand";
112
+
113
+ const handleChange = React.useCallback(
114
+ (next: string | null) => {
115
+ if (next !== null) onValueChange?.(next);
116
+ },
117
+ [onValueChange],
118
+ );
119
+
120
+ const [value, setValueInternal] = useControllableState<string | null>({
121
+ value: valueProp !== undefined ? valueProp : undefined,
122
+ defaultValue: defaultValue ?? null,
123
+ onChange: handleChange,
124
+ });
125
+
126
+ const setValue = React.useCallback(
127
+ (next: string) => {
128
+ setValueInternal(next);
129
+ },
130
+ [setValueInternal],
131
+ );
132
+
133
+ const rootClassName = radioGroup();
134
+
135
+ const ctx = React.useMemo<RadioGroupContextValue>(
136
+ () => ({ value, setValue, disabled, size, weight, tone }),
137
+ [value, setValue, disabled, size, weight, tone],
138
+ );
139
+
140
+ return (
141
+ <RadioGroupContext.Provider value={ctx}>
142
+ <view
143
+ {...(ref ? { ref: ref as React.Ref<SVGViewElement> } : {})}
144
+ className={clsx(rootClassName, className)}
145
+ {...nativeProps}
146
+ >
147
+ {children}
148
+ </view>
149
+ </RadioGroupContext.Provider>
150
+ );
151
+ });
152
+ RadioGroupRoot.displayName = "RadioGroupRoot";
153
+
154
+ ////////////////////////////////////////////////////////////////////////////////////
155
+
156
+ export interface RadioGroupItemProps {
157
+ value: string;
158
+ children?: React.ReactNode;
159
+ className?: string;
160
+ disabled?: boolean;
161
+ }
162
+
163
+ export const RadioGroupItem = React.forwardRef<unknown, RadioGroupItemProps>((props, ref) => {
164
+ const {
165
+ value: itemValue,
166
+ disabled: itemDisabled = false,
167
+ children,
168
+ className,
169
+ ...nativeProps
170
+ } = props;
171
+ const {
172
+ value: groupValue,
173
+ setValue,
174
+ disabled: groupDisabled,
175
+ size,
176
+ weight,
177
+ tone,
178
+ } = useRadioGroupContext("RadioGroupItem");
179
+
180
+ const disabled = groupDisabled || itemDisabled;
181
+ const checked = groupValue === itemValue;
182
+
183
+ const { pressed, ...pressHandlers } = usePressTap({
184
+ disabled,
185
+ onTap: () => {
186
+ if (checked) return;
187
+ setValue(itemValue);
188
+ },
189
+ });
190
+
191
+ const rootClassName = radio({ weight, size, disabled }).root;
192
+
193
+ const itemCtx = React.useMemo<RadioGroupItemContextValue>(
194
+ () => ({
195
+ value: itemValue,
196
+ checked,
197
+ disabled,
198
+ pressed,
199
+ tone,
200
+ size,
201
+ }),
202
+ [itemValue, checked, disabled, pressed, tone, size],
203
+ );
204
+
205
+ return (
206
+ <RadioGroupItemContext.Provider value={itemCtx}>
207
+ <view
208
+ {...(ref ? { ref: ref as React.Ref<SVGViewElement> } : {})}
209
+ className={clsx(rootClassName, className)}
210
+ {...pressHandlers}
211
+ {...nativeProps}
212
+ >
213
+ {children}
214
+ </view>
215
+ </RadioGroupItemContext.Provider>
216
+ );
217
+ });
218
+ RadioGroupItem.displayName = "RadioGroupItem";
219
+
220
+ ////////////////////////////////////////////////////////////////////////////////////
221
+
222
+ export interface RadioGroupItemControlProps extends Pick<RadiomarkVariantProps, "tone"> {
223
+ children?: React.ReactNode;
224
+ className?: string;
225
+ }
226
+
227
+ export const RadioGroupItemControl = React.forwardRef<unknown, RadioGroupItemControlProps>(
228
+ (props, ref) => {
229
+ const [variantProps, restProps] = radiomark.splitVariantProps(props);
230
+ const { children, className, ...nativeProps } = restProps;
231
+ const {
232
+ checked,
233
+ disabled,
234
+ pressed,
235
+ tone: ctxTone,
236
+ size,
237
+ } = useRadioGroupItemContext("RadioGroupItemControl");
238
+
239
+ const tone: RadiomarkTone = variantProps.tone ?? ctxTone;
240
+
241
+ const classes = React.useMemo(
242
+ () => radiomark({ tone, size, checked, disabled, pressed }),
243
+ [tone, size, checked, disabled, pressed],
244
+ );
245
+
246
+ return (
247
+ <RadiomarkIconClassContext.Provider value={classes.icon}>
248
+ <view
249
+ {...(ref ? { ref: ref as React.Ref<SVGViewElement> } : {})}
250
+ className={clsx(classes.root, className)}
251
+ {...nativeProps}
252
+ >
253
+ {children}
254
+ </view>
255
+ </RadiomarkIconClassContext.Provider>
256
+ );
257
+ },
258
+ );
259
+ RadioGroupItemControl.displayName = "RadioGroupItemControl";
260
+
261
+ ////////////////////////////////////////////////////////////////////////////////////
262
+
263
+ type IconElementProps = {
264
+ className?: string;
265
+ style?: CSSProperties;
266
+ ref?: React.Ref<MainThread.Element>;
267
+ };
268
+
269
+ export interface RadioGroupItemIndicatorProps {
270
+ /** Icon rendered when not checked. Optional — falls back to default `<view>` dot when omitted. */
271
+ unchecked?: ReactElement<IconElementProps>;
272
+ /** Icon rendered when the item is checked. Optional — falls back to default `<view>` dot when omitted. */
273
+ checked?: ReactElement<IconElementProps>;
274
+ className?: string;
275
+ style?: CSSProperties;
276
+ }
277
+
278
+ export function RadioGroupItemIndicator(props: RadioGroupItemIndicatorProps) {
279
+ const { unchecked, checked: checkedIcon, className, style } = props;
280
+ const { checked, disabled, pressed, tone, size } =
281
+ useRadioGroupItemContext("RadioGroupItemIndicator");
282
+ const iconClassName = useRadiomarkIconClass("RadioGroupItemIndicator");
283
+ const { ref: tintRef } = useIconColor([checked, disabled, pressed, tone, size]);
284
+
285
+ const icon = checked ? checkedIcon : unchecked;
286
+
287
+ // 사용자가 custom icon 을 주지 않으면 default `<view>` 로 동그란 점을 그린다.
288
+ // radiomark.icon recipe 가 borderRadius/backgroundColor/width/height 를 적용해
289
+ // 라디오의 inner dot 모양을 재현. 웹 RadioGroup 의 default `<svg><circle/></svg>` 동작과 일치.
290
+ if (!icon || !isValidElement<IconElementProps>(icon)) {
291
+ return <view className={clsx(iconClassName, className)} style={style} />;
292
+ }
293
+
294
+ // lynx-monochrome-icon 이 `<image style={{ width, height, ...style }}>` 로 자체 default size 를 박으므로
295
+ // recipe 토큰에서 꺼낸 사이즈를 inline 으로 주입해 덮는다.
296
+ const iconSize = resolveRecipeToken(radiomarkVars, [
297
+ `size${capitalize(size)}`,
298
+ disabled ? "disabled" : "enabled",
299
+ "icon",
300
+ "size",
301
+ ]);
302
+
303
+ return cloneElement(icon, {
304
+ className: clsx(iconClassName, icon.props.className, className),
305
+ style: iconSize
306
+ ? { width: iconSize, height: iconSize, ...icon.props.style, ...style }
307
+ : { ...icon.props.style, ...style },
308
+ ref: tintRef,
309
+ });
310
+ }
311
+ RadioGroupItemIndicator.displayName = "RadioGroupItemIndicator";
312
+
313
+ ////////////////////////////////////////////////////////////////////////////////////
314
+
315
+ export interface RadioGroupItemLabelProps {
316
+ children?: React.ReactNode;
317
+ className?: string;
318
+ }
319
+
320
+ export const RadioGroupItemLabel = React.forwardRef<unknown, RadioGroupItemLabelProps>(
321
+ (props, ref) => {
322
+ const { children, className, ...nativeProps } = props;
323
+ const { disabled } = useRadioGroupItemContext("RadioGroupItemLabel");
324
+ const { size, weight } = useRadioGroupContext("RadioGroupItemLabel");
325
+ const labelClassName = radio({ weight, size, disabled }).label;
326
+
327
+ return (
328
+ <text
329
+ {...(ref ? { ref: ref as React.Ref<SVGTextElement> } : {})}
330
+ className={clsx(labelClassName, className)}
331
+ {...nativeProps}
332
+ >
333
+ {children}
334
+ </text>
335
+ );
336
+ },
337
+ );
338
+ RadioGroupItemLabel.displayName = "RadioGroupItemLabel";
@@ -0,0 +1,14 @@
1
+ export {
2
+ RadioGroupRoot,
3
+ RadioGroupItem,
4
+ RadioGroupItemControl,
5
+ RadioGroupItemIndicator,
6
+ RadioGroupItemLabel,
7
+ type RadioGroupRootProps,
8
+ type RadioGroupItemProps,
9
+ type RadioGroupItemControlProps,
10
+ type RadioGroupItemIndicatorProps,
11
+ type RadioGroupItemLabelProps,
12
+ } from "./RadioGroup";
13
+
14
+ export * as RadioGroup from "./RadioGroup.namespace";
@@ -0,0 +1,10 @@
1
+ export {
2
+ SwitchControl as Control,
3
+ SwitchLabel as Label,
4
+ SwitchRoot as Root,
5
+ SwitchThumb as Thumb,
6
+ type SwitchControlProps as ControlProps,
7
+ type SwitchLabelProps as LabelProps,
8
+ type SwitchRootProps as RootProps,
9
+ type SwitchThumbProps as ThumbProps,
10
+ } from "./Switch";
@@ -0,0 +1,191 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+
4
+ import { switchStyle } from "@seed-design/lynx-css/recipes/switch";
5
+ import type { SwitchVariantProps } from "@seed-design/lynx-css/recipes/switch";
6
+ import { switchmark } from "@seed-design/lynx-css/recipes/switchmark";
7
+ import type { SwitchmarkVariantProps } from "@seed-design/lynx-css/recipes/switchmark";
8
+
9
+ import { splitMultipleVariantsProps } from "../../utils/split-multiple-variants-props";
10
+ import { useControllableState } from "../../hooks/use-controllable-state";
11
+ import { usePressTap } from "../../hooks/use-press-tap";
12
+
13
+ /**
14
+ * @platform Lynx
15
+ *
16
+ * 웹 대비 미지원 기능:
17
+ * - HiddenInput / name / value / required / invalid: Lynx에 native form 제출 모델이 없음
18
+ * - focus / focusVisible: Lynx에 키보드 포커스 개념이 없음
19
+ * - onChange (raw DOM event): 의미 없음. 토글 이벤트는 onCheckedChange로만 노출
20
+ *
21
+ * 추후 rootage 토큰 확장 시 추가 예정:
22
+ * - pressed boolean variant: switchmark rootage spec 에 pressed 상태가 추가되면
23
+ * switchmark recipe 와 Switch 컴포넌트에 boolean variant 로 노출.
24
+ */
25
+
26
+ type SwitchSize = NonNullable<SwitchVariantProps["size"]>;
27
+ type SwitchTone = NonNullable<SwitchmarkVariantProps["tone"]>;
28
+
29
+ interface SwitchContextValue {
30
+ checked: boolean;
31
+ disabled: boolean;
32
+ size: SwitchSize;
33
+ tone: SwitchTone;
34
+ }
35
+
36
+ const SwitchContext = React.createContext<SwitchContextValue | null>(null);
37
+
38
+ function useSwitchContext(consumer: string): SwitchContextValue {
39
+ const ctx = React.useContext(SwitchContext);
40
+ if (!ctx) {
41
+ throw new Error(`<${consumer}/> must be rendered inside <SwitchRoot/>.`);
42
+ }
43
+ return ctx;
44
+ }
45
+
46
+ const SwitchmarkThumbClassContext = React.createContext<string | null>(null);
47
+
48
+ function useSwitchmarkThumbClass(consumer: string): string {
49
+ const ctx = React.useContext(SwitchmarkThumbClassContext);
50
+ if (!ctx) {
51
+ throw new Error(`<${consumer}/> must be rendered inside <SwitchControl/>.`);
52
+ }
53
+ return ctx;
54
+ }
55
+
56
+ ////////////////////////////////////////////////////////////////////////////////////
57
+
58
+ export interface SwitchRootProps extends SwitchVariantProps, Omit<SwitchmarkVariantProps, "size"> {
59
+ children?: React.ReactNode;
60
+ className?: string;
61
+ checked?: boolean;
62
+ defaultChecked?: boolean;
63
+ disabled?: boolean;
64
+ onCheckedChange?: (checked: boolean) => void;
65
+ }
66
+
67
+ export const SwitchRoot = React.forwardRef<unknown, SwitchRootProps>((props, ref) => {
68
+ const {
69
+ children,
70
+ className,
71
+ checked: checkedProp,
72
+ defaultChecked = false,
73
+ disabled = false,
74
+ onCheckedChange,
75
+ ...restProps
76
+ } = props;
77
+ const [{ switch: switchVariantProps, switchmark: switchmarkVariantProps }, nativeProps] =
78
+ splitMultipleVariantsProps(restProps, { switch: switchStyle, switchmark });
79
+ const size: SwitchSize = switchVariantProps.size ?? "32";
80
+ const tone: SwitchTone = switchmarkVariantProps.tone ?? "brand";
81
+
82
+ const [checked, setChecked] = useControllableState({
83
+ value: checkedProp,
84
+ defaultValue: defaultChecked,
85
+ onChange: onCheckedChange,
86
+ });
87
+
88
+ const { pressed: _pressed, ...pressHandlers } = usePressTap({
89
+ disabled,
90
+ onTap: () => setChecked(!checked),
91
+ });
92
+
93
+ const rootClassName = switchStyle({ size }).root;
94
+
95
+ const ctx = React.useMemo<SwitchContextValue>(
96
+ () => ({ checked, disabled, size, tone }),
97
+ [checked, disabled, size, tone],
98
+ );
99
+
100
+ return (
101
+ <SwitchContext.Provider value={ctx}>
102
+ <view
103
+ {...(ref ? { ref: ref as React.Ref<SVGViewElement> } : {})}
104
+ className={clsx(rootClassName, className)}
105
+ {...pressHandlers}
106
+ {...nativeProps}
107
+ >
108
+ {children}
109
+ </view>
110
+ </SwitchContext.Provider>
111
+ );
112
+ });
113
+ SwitchRoot.displayName = "SwitchRoot";
114
+
115
+ ////////////////////////////////////////////////////////////////////////////////////
116
+
117
+ export interface SwitchControlProps extends Pick<SwitchmarkVariantProps, "tone" | "size"> {
118
+ children?: React.ReactNode;
119
+ className?: string;
120
+ }
121
+
122
+ export const SwitchControl = React.forwardRef<unknown, SwitchControlProps>((props, ref) => {
123
+ const [variantProps, restProps] = switchmark.splitVariantProps(props);
124
+ const { children, className, ...nativeProps } = restProps;
125
+ const { checked, disabled, tone: ctxTone, size: ctxSize } = useSwitchContext("SwitchControl");
126
+
127
+ const tone: SwitchTone = variantProps.tone ?? ctxTone;
128
+ const size: SwitchSize = variantProps.size ?? ctxSize;
129
+
130
+ const classes = React.useMemo(
131
+ () => switchmark({ tone, size, checked, disabled }),
132
+ [tone, size, checked, disabled],
133
+ );
134
+
135
+ return (
136
+ <SwitchmarkThumbClassContext.Provider value={classes.thumb}>
137
+ <view
138
+ {...(ref ? { ref: ref as React.Ref<SVGViewElement> } : {})}
139
+ className={clsx(classes.root, className)}
140
+ {...nativeProps}
141
+ >
142
+ {children}
143
+ </view>
144
+ </SwitchmarkThumbClassContext.Provider>
145
+ );
146
+ });
147
+ SwitchControl.displayName = "SwitchControl";
148
+
149
+ ////////////////////////////////////////////////////////////////////////////////////
150
+
151
+ export interface SwitchThumbProps {
152
+ className?: string;
153
+ }
154
+
155
+ export const SwitchThumb = React.forwardRef<unknown, SwitchThumbProps>((props, ref) => {
156
+ const { className, ...nativeProps } = props;
157
+ const thumbClassName = useSwitchmarkThumbClass("SwitchThumb");
158
+
159
+ return (
160
+ <view
161
+ {...(ref ? { ref: ref as React.Ref<SVGViewElement> } : {})}
162
+ className={clsx(thumbClassName, className)}
163
+ {...nativeProps}
164
+ />
165
+ );
166
+ });
167
+ SwitchThumb.displayName = "SwitchThumb";
168
+
169
+ ////////////////////////////////////////////////////////////////////////////////////
170
+
171
+ export interface SwitchLabelProps {
172
+ children?: React.ReactNode;
173
+ className?: string;
174
+ }
175
+
176
+ export const SwitchLabel = React.forwardRef<unknown, SwitchLabelProps>((props, ref) => {
177
+ const { children, className, ...nativeProps } = props;
178
+ const { disabled, size } = useSwitchContext("SwitchLabel");
179
+ const labelClassName = switchStyle({ size, disabled }).label;
180
+
181
+ return (
182
+ <text
183
+ {...(ref ? { ref: ref as React.Ref<SVGTextElement> } : {})}
184
+ className={clsx(labelClassName, className)}
185
+ {...nativeProps}
186
+ >
187
+ {children}
188
+ </text>
189
+ );
190
+ });
191
+ SwitchLabel.displayName = "SwitchLabel";
@@ -0,0 +1,12 @@
1
+ export {
2
+ SwitchRoot,
3
+ SwitchControl,
4
+ SwitchThumb,
5
+ SwitchLabel,
6
+ type SwitchRootProps,
7
+ type SwitchControlProps,
8
+ type SwitchThumbProps,
9
+ type SwitchLabelProps,
10
+ } from "./Switch";
11
+
12
+ export * as Switch from "./Switch.namespace";
@@ -0,0 +1,8 @@
1
+ export {
2
+ TagGroupItem as Item,
3
+ TagGroupItemLabel as ItemLabel,
4
+ TagGroupRoot as Root,
5
+ type TagGroupItemLabelProps as ItemLabelProps,
6
+ type TagGroupItemProps as ItemProps,
7
+ type TagGroupRootProps as RootProps,
8
+ } from "./TagGroup";