@seed-design/lynx-react 0.0.0-alpha-20260622094810

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 (196) hide show
  1. package/lib/_virtual/_rolldown/runtime.js +13 -0
  2. package/lib/components/ActionButton/ActionButton.d.ts +49 -0
  3. package/lib/components/ActionButton/ActionButton.d.ts.map +1 -0
  4. package/lib/components/ActionButton/ActionButton.jsx +158 -0
  5. package/lib/components/ActionButton/index.d.ts +2 -0
  6. package/lib/components/ActionButton/index.d.ts.map +1 -0
  7. package/lib/components/AppBar/AppBar.d.ts +35 -0
  8. package/lib/components/AppBar/AppBar.d.ts.map +1 -0
  9. package/lib/components/AppBar/AppBar.jsx +132 -0
  10. package/lib/components/AppBar/AppBar.namespace.d.ts +2 -0
  11. package/lib/components/AppBar/AppBar.namespace.d.ts.map +1 -0
  12. package/lib/components/AppBar/AppBar.namespace.js +15 -0
  13. package/lib/components/AppBar/context.d.ts +13 -0
  14. package/lib/components/AppBar/context.d.ts.map +1 -0
  15. package/lib/components/AppBar/context.js +12 -0
  16. package/lib/components/AppBar/index.d.ts +3 -0
  17. package/lib/components/AppBar/index.d.ts.map +1 -0
  18. package/lib/components/AppBar/useAppBar.d.ts +15 -0
  19. package/lib/components/AppBar/useAppBar.d.ts.map +1 -0
  20. package/lib/components/AppBar/useAppBar.js +72 -0
  21. package/lib/components/Badge/Badge.d.ts +14 -0
  22. package/lib/components/Badge/Badge.d.ts.map +1 -0
  23. package/lib/components/Badge/Badge.jsx +15 -0
  24. package/lib/components/Badge/index.d.ts +2 -0
  25. package/lib/components/Badge/index.d.ts.map +1 -0
  26. package/lib/components/BottomSheet/BottomSheet.d.ts +87 -0
  27. package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -0
  28. package/lib/components/BottomSheet/BottomSheet.jsx +163 -0
  29. package/lib/components/BottomSheet/BottomSheet.namespace.d.ts +2 -0
  30. package/lib/components/BottomSheet/BottomSheet.namespace.d.ts.map +1 -0
  31. package/lib/components/BottomSheet/BottomSheet.namespace.js +18 -0
  32. package/lib/components/BottomSheet/index.d.ts +3 -0
  33. package/lib/components/BottomSheet/index.d.ts.map +1 -0
  34. package/lib/components/Box/Box.d.ts +20 -0
  35. package/lib/components/Box/Box.d.ts.map +1 -0
  36. package/lib/components/Box/Box.jsx +14 -0
  37. package/lib/components/Box/index.d.ts +2 -0
  38. package/lib/components/Box/index.d.ts.map +1 -0
  39. package/lib/components/Checkbox/Checkbox.d.ts +35 -0
  40. package/lib/components/Checkbox/Checkbox.d.ts.map +1 -0
  41. package/lib/components/Checkbox/Checkbox.jsx +130 -0
  42. package/lib/components/Checkbox/Checkbox.namespace.d.ts +2 -0
  43. package/lib/components/Checkbox/Checkbox.namespace.d.ts.map +1 -0
  44. package/lib/components/Checkbox/Checkbox.namespace.js +12 -0
  45. package/lib/components/Checkbox/index.d.ts +3 -0
  46. package/lib/components/Checkbox/index.d.ts.map +1 -0
  47. package/lib/components/Icon/Icon.d.ts +37 -0
  48. package/lib/components/Icon/Icon.d.ts.map +1 -0
  49. package/lib/components/Icon/Icon.jsx +143 -0
  50. package/lib/components/Icon/index.d.ts +2 -0
  51. package/lib/components/Icon/index.d.ts.map +1 -0
  52. package/lib/components/ProgressCircle/ProgressCircle.d.ts +25 -0
  53. package/lib/components/ProgressCircle/ProgressCircle.d.ts.map +1 -0
  54. package/lib/components/ProgressCircle/ProgressCircle.jsx +281 -0
  55. package/lib/components/ProgressCircle/ProgressCircle.namespace.d.ts +2 -0
  56. package/lib/components/ProgressCircle/ProgressCircle.namespace.d.ts.map +1 -0
  57. package/lib/components/ProgressCircle/ProgressCircle.namespace.js +9 -0
  58. package/lib/components/ProgressCircle/index.d.ts +3 -0
  59. package/lib/components/ProgressCircle/index.d.ts.map +1 -0
  60. package/lib/components/RadioGroup/RadioGroup.d.ts +34 -0
  61. package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -0
  62. package/lib/components/RadioGroup/RadioGroup.jsx +160 -0
  63. package/lib/components/RadioGroup/RadioGroup.namespace.d.ts +2 -0
  64. package/lib/components/RadioGroup/RadioGroup.namespace.d.ts.map +1 -0
  65. package/lib/components/RadioGroup/RadioGroup.namespace.js +12 -0
  66. package/lib/components/RadioGroup/index.d.ts +3 -0
  67. package/lib/components/RadioGroup/index.d.ts.map +1 -0
  68. package/lib/components/Stack/Stack.d.ts +33 -0
  69. package/lib/components/Stack/Stack.d.ts.map +1 -0
  70. package/lib/components/Stack/Stack.jsx +39 -0
  71. package/lib/components/Stack/index.d.ts +2 -0
  72. package/lib/components/Stack/index.d.ts.map +1 -0
  73. package/lib/components/Switch/Switch.d.ts +21 -0
  74. package/lib/components/Switch/Switch.d.ts.map +1 -0
  75. package/lib/components/Switch/Switch.jsx +101 -0
  76. package/lib/components/Switch/Switch.namespace.d.ts +2 -0
  77. package/lib/components/Switch/Switch.namespace.d.ts.map +1 -0
  78. package/lib/components/Switch/Switch.namespace.js +11 -0
  79. package/lib/components/Switch/index.d.ts +3 -0
  80. package/lib/components/Switch/index.d.ts.map +1 -0
  81. package/lib/components/TagGroup/TagGroup.d.ts +34 -0
  82. package/lib/components/TagGroup/TagGroup.d.ts.map +1 -0
  83. package/lib/components/TagGroup/TagGroup.jsx +57 -0
  84. package/lib/components/TagGroup/TagGroup.namespace.d.ts +2 -0
  85. package/lib/components/TagGroup/TagGroup.namespace.d.ts.map +1 -0
  86. package/lib/components/TagGroup/TagGroup.namespace.js +10 -0
  87. package/lib/components/TagGroup/index.d.ts +3 -0
  88. package/lib/components/TagGroup/index.d.ts.map +1 -0
  89. package/lib/components/Text/Text.d.ts +7 -0
  90. package/lib/components/Text/Text.d.ts.map +1 -0
  91. package/lib/components/Text/Text.jsx +28 -0
  92. package/lib/components/Text/index.d.ts +2 -0
  93. package/lib/components/Text/index.d.ts.map +1 -0
  94. package/lib/components/index.d.ts +14 -0
  95. package/lib/components/index.d.ts.map +1 -0
  96. package/lib/hooks/index.d.ts +5 -0
  97. package/lib/hooks/index.d.ts.map +1 -0
  98. package/lib/hooks/useControllableState.d.ts +15 -0
  99. package/lib/hooks/useControllableState.d.ts.map +1 -0
  100. package/lib/hooks/useControllableState.js +27 -0
  101. package/lib/hooks/useIconColor.d.ts +19 -0
  102. package/lib/hooks/useIconColor.d.ts.map +1 -0
  103. package/lib/hooks/useIconColor.js +38 -0
  104. package/lib/hooks/usePressTap.d.ts +27 -0
  105. package/lib/hooks/usePressTap.d.ts.map +1 -0
  106. package/lib/hooks/usePressTap.js +35 -0
  107. package/lib/hooks/useSafeArea.d.ts +19 -0
  108. package/lib/hooks/useSafeArea.d.ts.map +1 -0
  109. package/lib/hooks/useSafeArea.js +33 -0
  110. package/lib/index.d.ts +5 -0
  111. package/lib/index.d.ts.map +1 -0
  112. package/lib/index.js +29 -0
  113. package/lib/types.d.ts +51 -0
  114. package/lib/types.d.ts.map +1 -0
  115. package/lib/utils/children.d.ts +3 -0
  116. package/lib/utils/children.d.ts.map +1 -0
  117. package/lib/utils/children.js +8 -0
  118. package/lib/utils/create-slot-recipe-context.d.ts +27 -0
  119. package/lib/utils/create-slot-recipe-context.d.ts.map +1 -0
  120. package/lib/utils/create-slot-recipe-context.jsx +94 -0
  121. package/lib/utils/get-seed-class-name.d.ts +22 -0
  122. package/lib/utils/get-seed-class-name.d.ts.map +1 -0
  123. package/lib/utils/get-seed-class-name.js +28 -0
  124. package/lib/utils/index.d.ts +5 -0
  125. package/lib/utils/index.d.ts.map +1 -0
  126. package/lib/utils/resolve-recipe-token.d.ts +38 -0
  127. package/lib/utils/resolve-recipe-token.d.ts.map +1 -0
  128. package/lib/utils/resolve-recipe-token.js +49 -0
  129. package/lib/utils/split-multiple-variants-props.d.ts +29 -0
  130. package/lib/utils/split-multiple-variants-props.d.ts.map +1 -0
  131. package/lib/utils/split-multiple-variants-props.js +34 -0
  132. package/lib/utils/styled.d.ts +83 -0
  133. package/lib/utils/styled.d.ts.map +1 -0
  134. package/lib/utils/styled.js +144 -0
  135. package/package.json +54 -0
  136. package/src/components/ActionButton/ActionButton.test.tsx +167 -0
  137. package/src/components/ActionButton/ActionButton.tsx +363 -0
  138. package/src/components/ActionButton/index.ts +1 -0
  139. package/src/components/AppBar/AppBar.namespace.ts +18 -0
  140. package/src/components/AppBar/AppBar.test.tsx +270 -0
  141. package/src/components/AppBar/AppBar.tsx +288 -0
  142. package/src/components/AppBar/context.ts +27 -0
  143. package/src/components/AppBar/index.ts +21 -0
  144. package/src/components/AppBar/useAppBar.ts +100 -0
  145. package/src/components/Badge/Badge.test.tsx +54 -0
  146. package/src/components/Badge/Badge.tsx +32 -0
  147. package/src/components/Badge/index.ts +1 -0
  148. package/src/components/BottomSheet/BottomSheet.namespace.ts +24 -0
  149. package/src/components/BottomSheet/BottomSheet.test.tsx +219 -0
  150. package/src/components/BottomSheet/BottomSheet.tsx +423 -0
  151. package/src/components/BottomSheet/index.ts +27 -0
  152. package/src/components/Box/Box.test.tsx +85 -0
  153. package/src/components/Box/Box.tsx +39 -0
  154. package/src/components/Box/index.ts +1 -0
  155. package/src/components/Checkbox/Checkbox.namespace.ts +12 -0
  156. package/src/components/Checkbox/Checkbox.tsx +282 -0
  157. package/src/components/Checkbox/index.ts +14 -0
  158. package/src/components/Icon/Icon.tsx +265 -0
  159. package/src/components/Icon/index.ts +8 -0
  160. package/src/components/ProgressCircle/ProgressCircle.namespace.ts +5 -0
  161. package/src/components/ProgressCircle/ProgressCircle.tsx +424 -0
  162. package/src/components/ProgressCircle/index.ts +9 -0
  163. package/src/components/RadioGroup/RadioGroup.namespace.ts +12 -0
  164. package/src/components/RadioGroup/RadioGroup.tsx +320 -0
  165. package/src/components/RadioGroup/index.ts +14 -0
  166. package/src/components/Stack/Stack.tsx +97 -0
  167. package/src/components/Stack/index.ts +1 -0
  168. package/src/components/Switch/Switch.namespace.ts +10 -0
  169. package/src/components/Switch/Switch.tsx +200 -0
  170. package/src/components/Switch/index.ts +12 -0
  171. package/src/components/TagGroup/TagGroup.namespace.ts +8 -0
  172. package/src/components/TagGroup/TagGroup.tsx +127 -0
  173. package/src/components/TagGroup/index.ts +10 -0
  174. package/src/components/Text/Text.tsx +64 -0
  175. package/src/components/Text/index.ts +1 -0
  176. package/src/components/index.ts +13 -0
  177. package/src/hooks/index.ts +4 -0
  178. package/src/hooks/useControllableState.test.ts +120 -0
  179. package/src/hooks/useControllableState.ts +38 -0
  180. package/src/hooks/useIconColor.test.ts +13 -0
  181. package/src/hooks/useIconColor.ts +67 -0
  182. package/src/hooks/usePressTap.test.ts +129 -0
  183. package/src/hooks/usePressTap.ts +58 -0
  184. package/src/hooks/useSafeArea.test.ts +83 -0
  185. package/src/hooks/useSafeArea.ts +56 -0
  186. package/src/index.ts +4 -0
  187. package/src/types.ts +76 -0
  188. package/src/utils/children.ts +13 -0
  189. package/src/utils/create-slot-recipe-context.test.tsx +96 -0
  190. package/src/utils/create-slot-recipe-context.tsx +187 -0
  191. package/src/utils/get-seed-class-name.ts +47 -0
  192. package/src/utils/index.ts +4 -0
  193. package/src/utils/resolve-recipe-token.ts +47 -0
  194. package/src/utils/split-multiple-variants-props.test.ts +61 -0
  195. package/src/utils/split-multiple-variants-props.ts +59 -0
  196. package/src/utils/styled.ts +442 -0
@@ -0,0 +1,363 @@
1
+ import { actionButton } from "@seed-design/lynx-css/recipes/action-button";
2
+ import type { ActionButtonVariantProps } from "@seed-design/lynx-css/recipes/action-button";
3
+ import { progressCircleVariantMap } from "@seed-design/lynx-css/recipes/progress-circle";
4
+ import { actionButton as actionButtonVars } from "@seed-design/lynx-css/vars/component";
5
+ import clsx from "clsx";
6
+ import * as React from "@lynx-js/react";
7
+ import { isValidElement, useMemo } from "@lynx-js/react";
8
+
9
+ import { usePressTap } from "../../hooks/usePressTap";
10
+ import type {
11
+ LynxElementProps,
12
+ LynxPressableProps,
13
+ LynxStyledElementProps,
14
+ LynxTextRef,
15
+ LynxTouchProps,
16
+ LynxViewProps,
17
+ LynxViewRef,
18
+ } from "../../types";
19
+ import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
20
+ import { toArray } from "../../utils/children";
21
+ import { capitalize, resolveRecipeToken } from "../../utils/resolve-recipe-token";
22
+ import { resolveFlexValue, type StyleProps } from "../../utils/styled";
23
+ import {
24
+ ProgressCircleRange,
25
+ ProgressCircleRoot,
26
+ type ProgressCircleRootProps,
27
+ } from "../ProgressCircle";
28
+ import {
29
+ Icon,
30
+ IconRequired,
31
+ IconSlotProvider,
32
+ PrefixIcon,
33
+ SuffixIcon,
34
+ getIconSlotName,
35
+ type IconProps,
36
+ type PrefixIconProps,
37
+ type SuffixIconProps,
38
+ } from "../Icon/Icon";
39
+
40
+ // Root/TextSlot 은 `withProvider("view", ...)` / `withContext("text", ...)` 를 쓰지 않는다.
41
+ // intrinsic string 인자는 `React.createElement("view", ...)` 로 컴파일되어 Lynx 컴파일러의
42
+ // 리터럴 JSX 정적 분석을 우회하고 `BackgroundSnapshot not found: view` 런타임 에러를 유발한다.
43
+ // (자세한 내용: `packages/lynx-react/AGENTS.md` 의 "Native tag literal JSX constraint" 섹션)
44
+ const { ClassNamesProvider, useClassNames, PropsProvider } = createSlotRecipeContext(actionButton);
45
+
46
+ ////////////////////////////////////////////////////////////////////////////////////
47
+
48
+ interface ActionButtonContentProps extends LynxElementProps {
49
+ isIconOnly: boolean;
50
+ icon?: IconProps["icon"];
51
+ prefixIcon?: PrefixIconProps["icon"];
52
+ suffixIcon?: SuffixIconProps["icon"];
53
+ }
54
+
55
+ interface ActionButtonAccessibilityProps {
56
+ "accessibility-label"?: LynxViewProps["accessibility-label"];
57
+ "accessibility-element"?: LynxViewProps["accessibility-element"];
58
+ "accessibility-traits"?: LynxViewProps["accessibility-traits"];
59
+ }
60
+
61
+ interface ActionButtonRootOwnProps
62
+ extends LynxStyledElementProps,
63
+ LynxTouchProps,
64
+ ActionButtonAccessibilityProps {}
65
+
66
+ interface ActionButtonRootProps extends ActionButtonVariantProps, ActionButtonRootOwnProps {}
67
+
68
+ function resolveProgressCircleSize(
69
+ actionButtonSize: ActionButtonVariantProps["size"],
70
+ ): ProgressCircleRootProps["size"] {
71
+ const size = actionButtonSize ?? "medium";
72
+ const token = resolveRecipeToken(actionButtonVars, [
73
+ `size${capitalize(size)}`,
74
+ "enabled",
75
+ "progressCircle",
76
+ "size",
77
+ ]);
78
+ const normalized = token?.endsWith("px") ? token.slice(0, -2) : token;
79
+
80
+ return progressCircleVariantMap.size.find((size) => size === normalized) ?? "16";
81
+ }
82
+
83
+ ////////////////////////////////////////////////////////////////////////////////////
84
+
85
+ const ActionButtonRoot = React.forwardRef<unknown, ActionButtonRootProps>((innerProps, ref) => {
86
+ const props = { layout: "withText" as const, ...innerProps };
87
+ const [variantProps, otherProps] = actionButton.splitVariantProps(props);
88
+ const classNames = actionButton(variantProps);
89
+ const {
90
+ className: userClassName,
91
+ children,
92
+ ...rest
93
+ } = otherProps as ActionButtonRootOwnProps & Record<string, unknown>;
94
+ const propsForContext = useMemo(
95
+ () => variantProps,
96
+ [
97
+ variantProps.variant,
98
+ variantProps.size,
99
+ variantProps.layout,
100
+ variantProps.pressed,
101
+ variantProps.disabled,
102
+ variantProps.loading,
103
+ ],
104
+ );
105
+ const iconSlotContextValue = useMemo(
106
+ () => ({
107
+ classNames: {
108
+ icon: classNames.icon,
109
+ prefixIcon: classNames.prefixIcon,
110
+ suffixIcon: classNames.suffixIcon,
111
+ },
112
+ deps: [
113
+ variantProps.variant ?? null,
114
+ variantProps.disabled ?? false,
115
+ variantProps.loading ?? false,
116
+ variantProps.pressed ?? false,
117
+ ],
118
+ }),
119
+ [
120
+ classNames.icon,
121
+ classNames.prefixIcon,
122
+ classNames.suffixIcon,
123
+ variantProps.variant,
124
+ variantProps.disabled,
125
+ variantProps.loading,
126
+ variantProps.pressed,
127
+ ],
128
+ );
129
+ return (
130
+ <ClassNamesProvider value={classNames}>
131
+ <PropsProvider value={propsForContext}>
132
+ <IconSlotProvider value={iconSlotContextValue}>
133
+ <view
134
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
135
+ {...rest}
136
+ className={clsx(classNames.root, userClassName)}
137
+ >
138
+ {children as React.ReactNode}
139
+ </view>
140
+ </IconSlotProvider>
141
+ </PropsProvider>
142
+ </ClassNamesProvider>
143
+ );
144
+ });
145
+ ActionButtonRoot.displayName = "ActionButtonRoot";
146
+
147
+ ////////////////////////////////////////////////////////////////////////////////////
148
+
149
+ interface ActionButtonTextSlotProps extends LynxElementProps {}
150
+
151
+ const ActionButtonTextSlot = React.forwardRef<unknown, ActionButtonTextSlotProps>((props, ref) => {
152
+ const { children, className: userClassName, ...rest } = props;
153
+ const classNames = useClassNames();
154
+ return (
155
+ <text
156
+ {...(ref ? { ref: ref as LynxTextRef } : {})}
157
+ {...rest}
158
+ className={clsx(classNames.text, userClassName)}
159
+ >
160
+ {children}
161
+ </text>
162
+ );
163
+ });
164
+ ActionButtonTextSlot.displayName = "ActionButtonTextSlot";
165
+
166
+ ////////////////////////////////////////////////////////////////////////////////////
167
+
168
+ function ActionButtonContent({
169
+ children,
170
+ isIconOnly,
171
+ icon,
172
+ prefixIcon,
173
+ suffixIcon,
174
+ }: ActionButtonContentProps) {
175
+ const childArray = toArray(children);
176
+ const prefixIconChildren: React.ReactNode[] = [];
177
+ const suffixIconChildren: React.ReactNode[] = [];
178
+ const iconChildren: React.ReactNode[] = [];
179
+ const textChildren: React.ReactNode[] = [];
180
+
181
+ for (const child of childArray) {
182
+ const slotName = getIconSlotName(child);
183
+
184
+ if (slotName === "prefixIcon") {
185
+ prefixIconChildren.push(child);
186
+ continue;
187
+ }
188
+ if (slotName === "suffixIcon") {
189
+ suffixIconChildren.push(child);
190
+ continue;
191
+ }
192
+ if (slotName === "icon") {
193
+ iconChildren.push(child);
194
+ continue;
195
+ }
196
+
197
+ textChildren.push(child);
198
+ }
199
+
200
+ if (isIconOnly) {
201
+ if (icon != null && isValidElement(icon)) return <Icon icon={icon} />;
202
+ return iconChildren.length > 0 ? <>{iconChildren}</> : null;
203
+ }
204
+
205
+ return (
206
+ <>
207
+ {prefixIcon != null && isValidElement(prefixIcon) ? <PrefixIcon icon={prefixIcon} /> : null}
208
+ {prefixIconChildren}
209
+ {textChildren.length > 0 ? <ActionButtonTextSlot>{textChildren}</ActionButtonTextSlot> : null}
210
+ {suffixIconChildren}
211
+ {suffixIcon != null && isValidElement(suffixIcon) ? <SuffixIcon icon={suffixIcon} /> : null}
212
+ </>
213
+ );
214
+ }
215
+
216
+ ////////////////////////////////////////////////////////////////////////////////////
217
+
218
+ function ActionButtonLoadingContent(props: ActionButtonContentProps) {
219
+ const classNames = useClassNames();
220
+
221
+ return (
222
+ <view className={classNames.content}>
223
+ <ActionButtonContent {...props} />
224
+ </view>
225
+ );
226
+ }
227
+
228
+ ////////////////////////////////////////////////////////////////////////////////////
229
+
230
+ function ActionButtonLoadingIndicator({ size }: { size: ActionButtonVariantProps["size"] }) {
231
+ const classNames = useClassNames();
232
+ const progressCircleSize = resolveProgressCircleSize(size);
233
+
234
+ return (
235
+ <view className={classNames.loadingIndicator}>
236
+ <ProgressCircleRoot size={progressCircleSize} tone="inherit">
237
+ <ProgressCircleRange />
238
+ </ProgressCircleRoot>
239
+ </view>
240
+ );
241
+ }
242
+
243
+ ////////////////////////////////////////////////////////////////////////////////////
244
+
245
+ /**
246
+ * @platform Lynx
247
+ *
248
+ * 웹 대비 차이:
249
+ * - 아이콘 렌더링: 웹의 SVG `currentColor` 대신 Lynx `<image>` 의 `tint-color` 를
250
+ * `Icon` / `PrefixIcon` / `SuffixIcon` wrapper 가 동기화한다.
251
+ * - 호환 API: 기존 `prefixIcon` / `suffixIcon` / `icon` prop 도 유지한다.
252
+ * - 미지원 prop: `color`, `fontWeight`, `bleedX`, `bleedY` (CSS variable 동적 주입 제한)
253
+ *
254
+ * ```tsx
255
+ * import IconPlusFill from "@karrotmarket/lynx-monochrome-icon/IconPlusFill";
256
+ * import IconChevronDownFill from "@karrotmarket/lynx-monochrome-icon/IconChevronDownFill";
257
+ * import { Icon, PrefixIcon, SuffixIcon } from "@seed-design/lynx-react";
258
+ *
259
+ * // withText (기본)
260
+ * <ActionButton variant="brandSolid">
261
+ * <PrefixIcon icon={<IconPlusFill />} />
262
+ * 라벨
263
+ * <SuffixIcon icon={<IconChevronDownFill />} />
264
+ * </ActionButton>
265
+ *
266
+ * // iconOnly — `<Icon />` child 와 `accessibility-label` 필수
267
+ * <ActionButton
268
+ * layout="iconOnly"
269
+ * variant="neutralSolid"
270
+ * accessibility-label="추가"
271
+ * >
272
+ * <Icon icon={<IconPlusFill />} />
273
+ * </ActionButton>
274
+ * ```
275
+ */
276
+ export interface ActionButtonProps
277
+ extends Omit<ActionButtonVariantProps, "pressed">,
278
+ Pick<StyleProps, "flexGrow">,
279
+ LynxElementProps,
280
+ LynxPressableProps,
281
+ ActionButtonAccessibilityProps {
282
+ icon?: IconProps["icon"];
283
+ prefixIcon?: PrefixIconProps["icon"];
284
+ suffixIcon?: SuffixIconProps["icon"];
285
+ }
286
+
287
+ export const ActionButton = React.forwardRef<unknown, ActionButtonProps>((props, ref) => {
288
+ const {
289
+ children,
290
+ flexGrow,
291
+ layout,
292
+ icon,
293
+ prefixIcon,
294
+ suffixIcon,
295
+ bindtap,
296
+ "main-thread:bindtap": mainThreadBindtap,
297
+ "accessibility-element": accessibilityElement = true,
298
+ "accessibility-label": accessibilityLabel,
299
+ "accessibility-traits": accessibilityTraits = "button",
300
+ ...variantAndRest
301
+ } = props;
302
+ const { disabled = false, loading = false } = variantAndRest;
303
+ const isInteractive = !disabled && !loading;
304
+ const isIconOnly = layout === "iconOnly";
305
+ const size = variantAndRest.size;
306
+
307
+ if (
308
+ process.env.NODE_ENV !== "production" &&
309
+ isIconOnly &&
310
+ accessibilityElement &&
311
+ !accessibilityLabel
312
+ ) {
313
+ console.warn(
314
+ 'ActionButton: `layout="iconOnly"` requires `accessibility-label` for accessibility.',
315
+ );
316
+ }
317
+
318
+ const { pressed, ...pressTapHandlers } = usePressTap({
319
+ disabled: !isInteractive,
320
+ onTap: bindtap,
321
+ mainThreadOnTap: mainThreadBindtap,
322
+ });
323
+
324
+ return (
325
+ <IconRequired enabled={isIconOnly}>
326
+ <ActionButtonRoot
327
+ {...variantAndRest}
328
+ layout={layout}
329
+ pressed={pressed}
330
+ ref={ref}
331
+ style={flexGrow != null ? { flexGrow: resolveFlexValue(flexGrow) } : undefined}
332
+ accessibility-element={accessibilityElement}
333
+ accessibility-label={accessibilityLabel}
334
+ accessibility-traits={accessibilityTraits}
335
+ {...pressTapHandlers}
336
+ >
337
+ {loading ? (
338
+ <>
339
+ <ActionButtonLoadingIndicator size={size} />
340
+ <ActionButtonLoadingContent
341
+ isIconOnly={isIconOnly}
342
+ icon={icon}
343
+ prefixIcon={prefixIcon}
344
+ suffixIcon={suffixIcon}
345
+ >
346
+ {children}
347
+ </ActionButtonLoadingContent>
348
+ </>
349
+ ) : (
350
+ <ActionButtonContent
351
+ isIconOnly={isIconOnly}
352
+ icon={icon}
353
+ prefixIcon={prefixIcon}
354
+ suffixIcon={suffixIcon}
355
+ >
356
+ {children}
357
+ </ActionButtonContent>
358
+ )}
359
+ </ActionButtonRoot>
360
+ </IconRequired>
361
+ );
362
+ });
363
+ ActionButton.displayName = "ActionButton";
@@ -0,0 +1 @@
1
+ export { ActionButton, type ActionButtonProps } from "./ActionButton";
@@ -0,0 +1,18 @@
1
+ export {
2
+ AppBarIconButton as IconButton,
3
+ AppBarLeft as Left,
4
+ AppBarMain as Main,
5
+ AppBarRight as Right,
6
+ AppBarRoot as Root,
7
+ AppBarSlot as Slot,
8
+ AppBarSubtitle as Subtitle,
9
+ AppBarTitle as Title,
10
+ type AppBarIconButtonProps as IconButtonProps,
11
+ type AppBarLeftProps as LeftProps,
12
+ type AppBarMainProps as MainProps,
13
+ type AppBarRightProps as RightProps,
14
+ type AppBarRootProps as RootProps,
15
+ type AppBarSlotProps as SlotProps,
16
+ type AppBarSubtitleProps as SubtitleProps,
17
+ type AppBarTitleProps as TitleProps,
18
+ } from "./AppBar";
@@ -0,0 +1,270 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ act,
4
+ fireEvent,
5
+ getQueriesForElement,
6
+ render,
7
+ waitFor,
8
+ } from "@lynx-js/react/testing-library";
9
+ import { afterEach, describe, expect, it, vi } from "vitest";
10
+
11
+ import * as AppBar from "./AppBar.namespace";
12
+
13
+ type TestSystemInfo = { platform?: string };
14
+
15
+ interface TestLynxGlobal {
16
+ SystemInfo?: TestSystemInfo;
17
+ lynx?: {
18
+ __globalProps?: {
19
+ safeAreaInsetTop?: number;
20
+ safeAreaInsetBottom?: number;
21
+ };
22
+ };
23
+ lynxTestingEnv?: {
24
+ backgroundThread: {
25
+ globalThis: TestLynxGlobal;
26
+ };
27
+ mainThread: {
28
+ globalThis: TestLynxGlobal;
29
+ };
30
+ };
31
+ }
32
+
33
+ function getRenderedQueries() {
34
+ return getQueriesForElement(getAppBarRoot());
35
+ }
36
+
37
+ function getRenderedRoot() {
38
+ const root = elementTree.root;
39
+
40
+ if (!root) {
41
+ throw new Error("Expected Lynx render root to exist.");
42
+ }
43
+
44
+ return root;
45
+ }
46
+
47
+ function getAppBarRoot() {
48
+ const root = getRenderedRoot();
49
+
50
+ if (root.classList.contains("seed-app-bar__root")) return root;
51
+
52
+ const appBarRoot = root.querySelector<HTMLElement>(".seed-app-bar__root");
53
+ if (!appBarRoot) {
54
+ throw new Error("Expected AppBar root to exist.");
55
+ }
56
+
57
+ return appBarRoot;
58
+ }
59
+
60
+ function expectStyle(style: CSSStyleDeclaration, expected: Record<string, string>) {
61
+ for (const [key, value] of Object.entries(expected)) {
62
+ expect(style.getPropertyValue(key)).toBe(value);
63
+ }
64
+ }
65
+
66
+ function setSystemInfo(systemInfo: TestSystemInfo | undefined) {
67
+ const lynxTestingEnv = (globalThis as TestLynxGlobal).lynxTestingEnv;
68
+ const globals = [
69
+ globalThis as TestLynxGlobal,
70
+ lynxTestingEnv?.backgroundThread.globalThis,
71
+ lynxTestingEnv?.mainThread.globalThis,
72
+ ].filter((global): global is TestLynxGlobal => Boolean(global));
73
+
74
+ for (const global of globals) {
75
+ if (systemInfo == null) {
76
+ delete global.SystemInfo;
77
+ } else {
78
+ global.SystemInfo = systemInfo;
79
+ }
80
+ }
81
+ }
82
+
83
+ function setGlobalProps(
84
+ globalProps: NonNullable<NonNullable<TestLynxGlobal["lynx"]>["__globalProps"]>,
85
+ ) {
86
+ const lynxTestingEnv = (globalThis as TestLynxGlobal).lynxTestingEnv;
87
+ const globals = [
88
+ globalThis as TestLynxGlobal,
89
+ lynxTestingEnv?.backgroundThread.globalThis,
90
+ lynxTestingEnv?.mainThread.globalThis,
91
+ ].filter((global): global is TestLynxGlobal => Boolean(global));
92
+
93
+ for (const global of globals) {
94
+ global.lynx = {
95
+ ...global.lynx,
96
+ __globalProps: globalProps,
97
+ };
98
+ }
99
+ }
100
+
101
+ function stubPlatform(platform: string | undefined) {
102
+ setSystemInfo(platform == null ? {} : { platform });
103
+ }
104
+
105
+ describe("AppBar", () => {
106
+ afterEach(() => {
107
+ setSystemInfo({ platform: "iOS" });
108
+ setGlobalProps({});
109
+ vi.unstubAllGlobals();
110
+ });
111
+
112
+ it("uses android theme by default on Android", () => {
113
+ stubPlatform("Android");
114
+
115
+ render(<AppBar.Root />);
116
+
117
+ expect(getAppBarRoot()).toHaveClass("seed-app-bar__root--theme_android");
118
+ });
119
+
120
+ it("uses cupertino theme by default outside Android", () => {
121
+ stubPlatform("iOS");
122
+
123
+ render(<AppBar.Root />);
124
+
125
+ expect(getAppBarRoot()).toHaveClass("seed-app-bar__root--theme_cupertino");
126
+ });
127
+
128
+ it("uses cupertino theme when SystemInfo.platform is unavailable", () => {
129
+ stubPlatform(undefined);
130
+
131
+ render(<AppBar.Root />);
132
+
133
+ expect(getAppBarRoot()).toHaveClass("seed-app-bar__root--theme_cupertino");
134
+ });
135
+
136
+ it("lets explicit theme override the platform default", () => {
137
+ stubPlatform("Android");
138
+
139
+ render(<AppBar.Root theme="cupertino" />);
140
+
141
+ expect(getAppBarRoot()).toHaveClass("seed-app-bar__root--theme_cupertino");
142
+ expect(getAppBarRoot()).not.toHaveClass("seed-app-bar__root--theme_android");
143
+ });
144
+
145
+ it("applies safe area from useSafeArea to the root layout", () => {
146
+ setGlobalProps({ safeAreaInsetTop: 47 });
147
+
148
+ render(<AppBar.Root theme="cupertino" />);
149
+
150
+ expectStyle(getAppBarRoot().style, {
151
+ height: "calc(91px)",
152
+ "padding-top": "47px",
153
+ });
154
+ });
155
+
156
+ it("does not read the safe area CSS variable override as the JS layout source", () => {
157
+ setGlobalProps({ safeAreaInsetTop: 47 });
158
+
159
+ render(
160
+ <AppBar.Root
161
+ theme="cupertino"
162
+ style={{ "--seed-safe-area-top": "0px" } as AppBar.RootProps["style"]}
163
+ />,
164
+ );
165
+
166
+ expectStyle(getAppBarRoot().style, {
167
+ height: "calc(91px)",
168
+ "padding-top": "47px",
169
+ });
170
+ });
171
+
172
+ it("keeps Cupertino title-only main centered in the safe-area adjusted bar", () => {
173
+ setGlobalProps({ safeAreaInsetTop: 47 });
174
+
175
+ render(
176
+ <AppBar.Root theme="cupertino">
177
+ <AppBar.Main>
178
+ <AppBar.Title>Title</AppBar.Title>
179
+ </AppBar.Main>
180
+ </AppBar.Root>,
181
+ );
182
+
183
+ const main = getAppBarRoot().querySelector<HTMLElement>(".seed-app-bar-main__root");
184
+
185
+ expect(main).toBeInTheDocument();
186
+ expectStyle(main!.style, {
187
+ top: "47px",
188
+ bottom: "0px",
189
+ });
190
+ });
191
+
192
+ it("renders slots with recipe class names", () => {
193
+ render(
194
+ <AppBar.Root>
195
+ <AppBar.Left>
196
+ <AppBar.IconButton accessibility-label="Back" />
197
+ </AppBar.Left>
198
+ <AppBar.Main layout="withSubtitle">
199
+ <AppBar.Title>Title</AppBar.Title>
200
+ <AppBar.Subtitle>Subtitle</AppBar.Subtitle>
201
+ </AppBar.Main>
202
+ <AppBar.Right>
203
+ <AppBar.Slot>
204
+ <text>Done</text>
205
+ </AppBar.Slot>
206
+ </AppBar.Right>
207
+ </AppBar.Root>,
208
+ );
209
+
210
+ const { getByText } = getRenderedQueries();
211
+ const root = getAppBarRoot();
212
+
213
+ expect(root.querySelector(".seed-app-bar__left")).toBeInTheDocument();
214
+ expect(root.querySelector(".seed-app-bar__right")).toBeInTheDocument();
215
+ expect(root.querySelector(".seed-app-bar__iconButton")).toBeInTheDocument();
216
+ expect(root.querySelector(".seed-app-bar__custom")).toBeInTheDocument();
217
+ expect(getByText("Title")).toHaveClass("seed-app-bar-main__title");
218
+ expect(getByText("Subtitle")).toHaveClass("seed-app-bar-main__subtitle");
219
+ });
220
+
221
+ it("maps icon button labels to Lynx accessibility props", () => {
222
+ render(
223
+ <AppBar.Root>
224
+ <AppBar.Left>
225
+ <AppBar.IconButton accessibility-label="Back" />
226
+ </AppBar.Left>
227
+ </AppBar.Root>,
228
+ );
229
+
230
+ const iconButton = getAppBarRoot().querySelector(".seed-app-bar__iconButton");
231
+
232
+ expect(iconButton).toHaveAttribute("accessibility-label", "Back");
233
+ expect(iconButton).toHaveAttribute("accessibility-element", "true");
234
+ expect(iconButton).toHaveAttribute("accessibility-traits", "button");
235
+ });
236
+
237
+ it("updates centered title padding from left and right layout widths", async () => {
238
+ render(
239
+ <AppBar.Root>
240
+ <AppBar.Left>
241
+ <text>Left</text>
242
+ </AppBar.Left>
243
+ <AppBar.Main>
244
+ <AppBar.Title>Title</AppBar.Title>
245
+ </AppBar.Main>
246
+ <AppBar.Right>
247
+ <text>Right</text>
248
+ </AppBar.Right>
249
+ </AppBar.Root>,
250
+ );
251
+
252
+ const root = getAppBarRoot();
253
+ const main = root.querySelector(".seed-app-bar-main__root");
254
+ const left = root.querySelector(".seed-app-bar__left");
255
+ const right = root.querySelector(".seed-app-bar__right");
256
+
257
+ if (!main || !left || !right) {
258
+ throw new Error("Expected AppBar main, left, and right slots to exist.");
259
+ }
260
+
261
+ act(() => {
262
+ fireEvent.layoutchange(left, { width: 48 });
263
+ fireEvent.layoutchange(right, { width: 72 });
264
+ });
265
+
266
+ await waitFor(() => {
267
+ expect(main).toHaveStyle({ paddingLeft: "72px", paddingRight: "72px" });
268
+ });
269
+ });
270
+ });