@seed-design/lynx-react 0.1.0

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 (168) hide show
  1. package/lib/_virtual/_rolldown/runtime.js +13 -0
  2. package/lib/components/ActionButton/ActionButton.d.ts +44 -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/BottomSheet/BottomSheet.d.ts +87 -0
  8. package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -0
  9. package/lib/components/BottomSheet/BottomSheet.jsx +150 -0
  10. package/lib/components/BottomSheet/BottomSheet.namespace.d.ts +2 -0
  11. package/lib/components/BottomSheet/BottomSheet.namespace.d.ts.map +1 -0
  12. package/lib/components/BottomSheet/BottomSheet.namespace.js +18 -0
  13. package/lib/components/BottomSheet/index.d.ts +3 -0
  14. package/lib/components/BottomSheet/index.d.ts.map +1 -0
  15. package/lib/components/Box/Box.d.ts +20 -0
  16. package/lib/components/Box/Box.d.ts.map +1 -0
  17. package/lib/components/Box/Box.jsx +14 -0
  18. package/lib/components/Box/index.d.ts +2 -0
  19. package/lib/components/Box/index.d.ts.map +1 -0
  20. package/lib/components/Checkbox/Checkbox.d.ts +35 -0
  21. package/lib/components/Checkbox/Checkbox.d.ts.map +1 -0
  22. package/lib/components/Checkbox/Checkbox.jsx +130 -0
  23. package/lib/components/Checkbox/Checkbox.namespace.d.ts +2 -0
  24. package/lib/components/Checkbox/Checkbox.namespace.d.ts.map +1 -0
  25. package/lib/components/Checkbox/Checkbox.namespace.js +12 -0
  26. package/lib/components/Checkbox/index.d.ts +3 -0
  27. package/lib/components/Checkbox/index.d.ts.map +1 -0
  28. package/lib/components/Icon/Icon.d.ts +37 -0
  29. package/lib/components/Icon/Icon.d.ts.map +1 -0
  30. package/lib/components/Icon/Icon.jsx +143 -0
  31. package/lib/components/Icon/index.d.ts +2 -0
  32. package/lib/components/Icon/index.d.ts.map +1 -0
  33. package/lib/components/ProgressCircle/ProgressCircle.d.ts +25 -0
  34. package/lib/components/ProgressCircle/ProgressCircle.d.ts.map +1 -0
  35. package/lib/components/ProgressCircle/ProgressCircle.jsx +281 -0
  36. package/lib/components/ProgressCircle/ProgressCircle.namespace.d.ts +2 -0
  37. package/lib/components/ProgressCircle/ProgressCircle.namespace.d.ts.map +1 -0
  38. package/lib/components/ProgressCircle/ProgressCircle.namespace.js +9 -0
  39. package/lib/components/ProgressCircle/index.d.ts +3 -0
  40. package/lib/components/ProgressCircle/index.d.ts.map +1 -0
  41. package/lib/components/RadioGroup/RadioGroup.d.ts +34 -0
  42. package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -0
  43. package/lib/components/RadioGroup/RadioGroup.jsx +160 -0
  44. package/lib/components/RadioGroup/RadioGroup.namespace.d.ts +2 -0
  45. package/lib/components/RadioGroup/RadioGroup.namespace.d.ts.map +1 -0
  46. package/lib/components/RadioGroup/RadioGroup.namespace.js +12 -0
  47. package/lib/components/RadioGroup/index.d.ts +3 -0
  48. package/lib/components/RadioGroup/index.d.ts.map +1 -0
  49. package/lib/components/Stack/Stack.d.ts +33 -0
  50. package/lib/components/Stack/Stack.d.ts.map +1 -0
  51. package/lib/components/Stack/Stack.jsx +39 -0
  52. package/lib/components/Stack/index.d.ts +2 -0
  53. package/lib/components/Stack/index.d.ts.map +1 -0
  54. package/lib/components/Switch/Switch.d.ts +21 -0
  55. package/lib/components/Switch/Switch.d.ts.map +1 -0
  56. package/lib/components/Switch/Switch.jsx +101 -0
  57. package/lib/components/Switch/Switch.namespace.d.ts +2 -0
  58. package/lib/components/Switch/Switch.namespace.d.ts.map +1 -0
  59. package/lib/components/Switch/Switch.namespace.js +11 -0
  60. package/lib/components/Switch/index.d.ts +3 -0
  61. package/lib/components/Switch/index.d.ts.map +1 -0
  62. package/lib/components/TagGroup/TagGroup.d.ts +34 -0
  63. package/lib/components/TagGroup/TagGroup.d.ts.map +1 -0
  64. package/lib/components/TagGroup/TagGroup.jsx +57 -0
  65. package/lib/components/TagGroup/TagGroup.namespace.d.ts +2 -0
  66. package/lib/components/TagGroup/TagGroup.namespace.d.ts.map +1 -0
  67. package/lib/components/TagGroup/TagGroup.namespace.js +10 -0
  68. package/lib/components/TagGroup/index.d.ts +3 -0
  69. package/lib/components/TagGroup/index.d.ts.map +1 -0
  70. package/lib/components/Text/Text.d.ts +7 -0
  71. package/lib/components/Text/Text.d.ts.map +1 -0
  72. package/lib/components/Text/Text.jsx +28 -0
  73. package/lib/components/Text/index.d.ts +2 -0
  74. package/lib/components/Text/index.d.ts.map +1 -0
  75. package/lib/components/index.d.ts +12 -0
  76. package/lib/components/index.d.ts.map +1 -0
  77. package/lib/hooks/index.d.ts +5 -0
  78. package/lib/hooks/index.d.ts.map +1 -0
  79. package/lib/hooks/useControllableState.d.ts +15 -0
  80. package/lib/hooks/useControllableState.d.ts.map +1 -0
  81. package/lib/hooks/useControllableState.js +27 -0
  82. package/lib/hooks/useIconColor.d.ts +19 -0
  83. package/lib/hooks/useIconColor.d.ts.map +1 -0
  84. package/lib/hooks/useIconColor.js +38 -0
  85. package/lib/hooks/usePressTap.d.ts +27 -0
  86. package/lib/hooks/usePressTap.d.ts.map +1 -0
  87. package/lib/hooks/usePressTap.js +35 -0
  88. package/lib/hooks/useSafeArea.d.ts +19 -0
  89. package/lib/hooks/useSafeArea.d.ts.map +1 -0
  90. package/lib/hooks/useSafeArea.js +33 -0
  91. package/lib/index.d.ts +5 -0
  92. package/lib/index.d.ts.map +1 -0
  93. package/lib/index.js +26 -0
  94. package/lib/types.d.ts +33 -0
  95. package/lib/types.d.ts.map +1 -0
  96. package/lib/utils/children.d.ts +3 -0
  97. package/lib/utils/children.d.ts.map +1 -0
  98. package/lib/utils/children.js +8 -0
  99. package/lib/utils/create-slot-recipe-context.d.ts +27 -0
  100. package/lib/utils/create-slot-recipe-context.d.ts.map +1 -0
  101. package/lib/utils/create-slot-recipe-context.jsx +94 -0
  102. package/lib/utils/get-seed-class-name.d.ts +22 -0
  103. package/lib/utils/get-seed-class-name.d.ts.map +1 -0
  104. package/lib/utils/get-seed-class-name.js +28 -0
  105. package/lib/utils/index.d.ts +5 -0
  106. package/lib/utils/index.d.ts.map +1 -0
  107. package/lib/utils/resolve-recipe-token.d.ts +38 -0
  108. package/lib/utils/resolve-recipe-token.d.ts.map +1 -0
  109. package/lib/utils/resolve-recipe-token.js +49 -0
  110. package/lib/utils/split-multiple-variants-props.d.ts +29 -0
  111. package/lib/utils/split-multiple-variants-props.d.ts.map +1 -0
  112. package/lib/utils/split-multiple-variants-props.js +34 -0
  113. package/lib/utils/styled.d.ts +83 -0
  114. package/lib/utils/styled.d.ts.map +1 -0
  115. package/lib/utils/styled.js +144 -0
  116. package/package.json +54 -0
  117. package/src/components/ActionButton/ActionButton.test.tsx +137 -0
  118. package/src/components/ActionButton/ActionButton.tsx +340 -0
  119. package/src/components/ActionButton/index.ts +1 -0
  120. package/src/components/BottomSheet/BottomSheet.namespace.ts +24 -0
  121. package/src/components/BottomSheet/BottomSheet.test.tsx +153 -0
  122. package/src/components/BottomSheet/BottomSheet.tsx +397 -0
  123. package/src/components/BottomSheet/index.ts +27 -0
  124. package/src/components/Box/Box.test.tsx +85 -0
  125. package/src/components/Box/Box.tsx +39 -0
  126. package/src/components/Box/index.ts +1 -0
  127. package/src/components/Checkbox/Checkbox.namespace.ts +12 -0
  128. package/src/components/Checkbox/Checkbox.tsx +282 -0
  129. package/src/components/Checkbox/index.ts +14 -0
  130. package/src/components/Icon/Icon.tsx +265 -0
  131. package/src/components/Icon/index.ts +8 -0
  132. package/src/components/ProgressCircle/ProgressCircle.namespace.ts +5 -0
  133. package/src/components/ProgressCircle/ProgressCircle.tsx +424 -0
  134. package/src/components/ProgressCircle/index.ts +9 -0
  135. package/src/components/RadioGroup/RadioGroup.namespace.ts +12 -0
  136. package/src/components/RadioGroup/RadioGroup.tsx +320 -0
  137. package/src/components/RadioGroup/index.ts +14 -0
  138. package/src/components/Stack/Stack.tsx +97 -0
  139. package/src/components/Stack/index.ts +1 -0
  140. package/src/components/Switch/Switch.namespace.ts +10 -0
  141. package/src/components/Switch/Switch.tsx +200 -0
  142. package/src/components/Switch/index.ts +12 -0
  143. package/src/components/TagGroup/TagGroup.namespace.ts +8 -0
  144. package/src/components/TagGroup/TagGroup.tsx +127 -0
  145. package/src/components/TagGroup/index.ts +10 -0
  146. package/src/components/Text/Text.tsx +64 -0
  147. package/src/components/Text/index.ts +1 -0
  148. package/src/components/index.ts +11 -0
  149. package/src/hooks/index.ts +4 -0
  150. package/src/hooks/useControllableState.test.ts +120 -0
  151. package/src/hooks/useControllableState.ts +38 -0
  152. package/src/hooks/useIconColor.test.ts +13 -0
  153. package/src/hooks/useIconColor.ts +67 -0
  154. package/src/hooks/usePressTap.test.ts +129 -0
  155. package/src/hooks/usePressTap.ts +58 -0
  156. package/src/hooks/useSafeArea.test.ts +83 -0
  157. package/src/hooks/useSafeArea.ts +56 -0
  158. package/src/index.ts +4 -0
  159. package/src/types.ts +45 -0
  160. package/src/utils/children.ts +13 -0
  161. package/src/utils/create-slot-recipe-context.test.tsx +96 -0
  162. package/src/utils/create-slot-recipe-context.tsx +187 -0
  163. package/src/utils/get-seed-class-name.ts +47 -0
  164. package/src/utils/index.ts +4 -0
  165. package/src/utils/resolve-recipe-token.ts +47 -0
  166. package/src/utils/split-multiple-variants-props.test.ts +61 -0
  167. package/src/utils/split-multiple-variants-props.ts +59 -0
  168. package/src/utils/styled.ts +442 -0
@@ -0,0 +1,340 @@
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
+ LynxViewRef,
17
+ } from "../../types";
18
+ import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
19
+ import { toArray } from "../../utils/children";
20
+ import { capitalize, resolveRecipeToken } from "../../utils/resolve-recipe-token";
21
+ import { resolveFlexValue, type StyleProps } from "../../utils/styled";
22
+ import {
23
+ ProgressCircleRange,
24
+ ProgressCircleRoot,
25
+ type ProgressCircleRootProps,
26
+ } from "../ProgressCircle";
27
+ import {
28
+ Icon,
29
+ IconRequired,
30
+ IconSlotProvider,
31
+ PrefixIcon,
32
+ SuffixIcon,
33
+ getIconSlotName,
34
+ type IconProps,
35
+ type PrefixIconProps,
36
+ type SuffixIconProps,
37
+ } from "../Icon/Icon";
38
+
39
+ // Root/TextSlot 은 `withProvider("view", ...)` / `withContext("text", ...)` 를 쓰지 않는다.
40
+ // intrinsic string 인자는 `React.createElement("view", ...)` 로 컴파일되어 Lynx 컴파일러의
41
+ // 리터럴 JSX 정적 분석을 우회하고 `BackgroundSnapshot not found: view` 런타임 에러를 유발한다.
42
+ // (자세한 내용: `packages/lynx-react/AGENTS.md` 의 "Native tag literal JSX constraint" 섹션)
43
+ const { ClassNamesProvider, useClassNames, PropsProvider } = createSlotRecipeContext(actionButton);
44
+
45
+ ////////////////////////////////////////////////////////////////////////////////////
46
+
47
+ interface ActionButtonContentProps extends LynxElementProps {
48
+ isIconOnly: boolean;
49
+ icon?: IconProps["icon"];
50
+ prefixIcon?: PrefixIconProps["icon"];
51
+ suffixIcon?: SuffixIconProps["icon"];
52
+ }
53
+
54
+ interface ActionButtonRootOwnProps extends LynxStyledElementProps, LynxTouchProps {}
55
+
56
+ interface ActionButtonRootProps extends ActionButtonVariantProps, ActionButtonRootOwnProps {}
57
+
58
+ function resolveProgressCircleSize(
59
+ actionButtonSize: ActionButtonVariantProps["size"],
60
+ ): ProgressCircleRootProps["size"] {
61
+ const size = actionButtonSize ?? "medium";
62
+ const token = resolveRecipeToken(actionButtonVars, [
63
+ `size${capitalize(size)}`,
64
+ "enabled",
65
+ "progressCircle",
66
+ "size",
67
+ ]);
68
+ const normalized = token?.endsWith("px") ? token.slice(0, -2) : token;
69
+
70
+ return progressCircleVariantMap.size.find((size) => size === normalized) ?? "16";
71
+ }
72
+
73
+ ////////////////////////////////////////////////////////////////////////////////////
74
+
75
+ const ActionButtonRoot = React.forwardRef<unknown, ActionButtonRootProps>((innerProps, ref) => {
76
+ const props = { layout: "withText" as const, ...innerProps };
77
+ const [variantProps, otherProps] = actionButton.splitVariantProps(props);
78
+ const classNames = actionButton(variantProps);
79
+ const {
80
+ className: userClassName,
81
+ children,
82
+ ...rest
83
+ } = otherProps as ActionButtonRootOwnProps & Record<string, unknown>;
84
+ const propsForContext = useMemo(
85
+ () => variantProps,
86
+ [
87
+ variantProps.variant,
88
+ variantProps.size,
89
+ variantProps.layout,
90
+ variantProps.pressed,
91
+ variantProps.disabled,
92
+ variantProps.loading,
93
+ ],
94
+ );
95
+ const iconSlotContextValue = useMemo(
96
+ () => ({
97
+ classNames: {
98
+ icon: classNames.icon,
99
+ prefixIcon: classNames.prefixIcon,
100
+ suffixIcon: classNames.suffixIcon,
101
+ },
102
+ deps: [
103
+ variantProps.variant ?? null,
104
+ variantProps.disabled ?? false,
105
+ variantProps.loading ?? false,
106
+ variantProps.pressed ?? false,
107
+ ],
108
+ }),
109
+ [
110
+ classNames.icon,
111
+ classNames.prefixIcon,
112
+ classNames.suffixIcon,
113
+ variantProps.variant,
114
+ variantProps.disabled,
115
+ variantProps.loading,
116
+ variantProps.pressed,
117
+ ],
118
+ );
119
+ return (
120
+ <ClassNamesProvider value={classNames}>
121
+ <PropsProvider value={propsForContext}>
122
+ <IconSlotProvider value={iconSlotContextValue}>
123
+ <view
124
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
125
+ {...rest}
126
+ className={clsx(classNames.root, userClassName)}
127
+ >
128
+ {children as React.ReactNode}
129
+ </view>
130
+ </IconSlotProvider>
131
+ </PropsProvider>
132
+ </ClassNamesProvider>
133
+ );
134
+ });
135
+ ActionButtonRoot.displayName = "ActionButtonRoot";
136
+
137
+ ////////////////////////////////////////////////////////////////////////////////////
138
+
139
+ interface ActionButtonTextSlotProps extends LynxElementProps {}
140
+
141
+ const ActionButtonTextSlot = React.forwardRef<unknown, ActionButtonTextSlotProps>((props, ref) => {
142
+ const { children, className: userClassName, ...rest } = props;
143
+ const classNames = useClassNames();
144
+ return (
145
+ <text
146
+ {...(ref ? { ref: ref as LynxTextRef } : {})}
147
+ {...rest}
148
+ className={clsx(classNames.text, userClassName)}
149
+ >
150
+ {children}
151
+ </text>
152
+ );
153
+ });
154
+ ActionButtonTextSlot.displayName = "ActionButtonTextSlot";
155
+
156
+ ////////////////////////////////////////////////////////////////////////////////////
157
+
158
+ function ActionButtonContent({
159
+ children,
160
+ isIconOnly,
161
+ icon,
162
+ prefixIcon,
163
+ suffixIcon,
164
+ }: ActionButtonContentProps) {
165
+ const childArray = toArray(children);
166
+ const prefixIconChildren: React.ReactNode[] = [];
167
+ const suffixIconChildren: React.ReactNode[] = [];
168
+ const iconChildren: React.ReactNode[] = [];
169
+ const textChildren: React.ReactNode[] = [];
170
+
171
+ for (const child of childArray) {
172
+ const slotName = getIconSlotName(child);
173
+
174
+ if (slotName === "prefixIcon") {
175
+ prefixIconChildren.push(child);
176
+ continue;
177
+ }
178
+ if (slotName === "suffixIcon") {
179
+ suffixIconChildren.push(child);
180
+ continue;
181
+ }
182
+ if (slotName === "icon") {
183
+ iconChildren.push(child);
184
+ continue;
185
+ }
186
+
187
+ textChildren.push(child);
188
+ }
189
+
190
+ if (isIconOnly) {
191
+ if (icon != null && isValidElement(icon)) return <Icon icon={icon} />;
192
+ return iconChildren.length > 0 ? <>{iconChildren}</> : null;
193
+ }
194
+
195
+ return (
196
+ <>
197
+ {prefixIcon != null && isValidElement(prefixIcon) ? <PrefixIcon icon={prefixIcon} /> : null}
198
+ {prefixIconChildren}
199
+ {textChildren.length > 0 ? <ActionButtonTextSlot>{textChildren}</ActionButtonTextSlot> : null}
200
+ {suffixIconChildren}
201
+ {suffixIcon != null && isValidElement(suffixIcon) ? <SuffixIcon icon={suffixIcon} /> : null}
202
+ </>
203
+ );
204
+ }
205
+
206
+ ////////////////////////////////////////////////////////////////////////////////////
207
+
208
+ function ActionButtonLoadingContent(props: ActionButtonContentProps) {
209
+ const classNames = useClassNames();
210
+
211
+ return (
212
+ <view className={classNames.content}>
213
+ <ActionButtonContent {...props} />
214
+ </view>
215
+ );
216
+ }
217
+
218
+ ////////////////////////////////////////////////////////////////////////////////////
219
+
220
+ function ActionButtonLoadingIndicator({ size }: { size: ActionButtonVariantProps["size"] }) {
221
+ const classNames = useClassNames();
222
+ const progressCircleSize = resolveProgressCircleSize(size);
223
+
224
+ return (
225
+ <view className={classNames.loadingIndicator}>
226
+ <ProgressCircleRoot size={progressCircleSize} tone="inherit">
227
+ <ProgressCircleRange />
228
+ </ProgressCircleRoot>
229
+ </view>
230
+ );
231
+ }
232
+
233
+ ////////////////////////////////////////////////////////////////////////////////////
234
+
235
+ /**
236
+ * @platform Lynx
237
+ *
238
+ * 웹 대비 차이:
239
+ * - 아이콘 렌더링: 웹의 SVG `currentColor` 대신 Lynx `<image>` 의 `tint-color` 를
240
+ * `Icon` / `PrefixIcon` / `SuffixIcon` wrapper 가 동기화한다.
241
+ * - 호환 API: 기존 `prefixIcon` / `suffixIcon` / `icon` prop 도 유지한다.
242
+ * - 미지원 prop: `color`, `fontWeight`, `bleedX`, `bleedY` (CSS variable 동적 주입 제한)
243
+ *
244
+ * ```tsx
245
+ * import IconPlusFill from "@karrotmarket/lynx-monochrome-icon/IconPlusFill";
246
+ * import IconChevronDownFill from "@karrotmarket/lynx-monochrome-icon/IconChevronDownFill";
247
+ * import { Icon, PrefixIcon, SuffixIcon } from "@seed-design/lynx-react";
248
+ *
249
+ * // withText (기본)
250
+ * <ActionButton variant="brandSolid">
251
+ * <PrefixIcon icon={<IconPlusFill />} />
252
+ * 라벨
253
+ * <SuffixIcon icon={<IconChevronDownFill />} />
254
+ * </ActionButton>
255
+ *
256
+ * // iconOnly — `<Icon />` child 와 `aria-label` 필수
257
+ * <ActionButton
258
+ * layout="iconOnly"
259
+ * variant="neutralSolid"
260
+ * aria-label="추가"
261
+ * >
262
+ * <Icon icon={<IconPlusFill />} />
263
+ * </ActionButton>
264
+ * ```
265
+ */
266
+ export interface ActionButtonProps
267
+ extends Omit<ActionButtonVariantProps, "pressed">,
268
+ Pick<StyleProps, "flexGrow">,
269
+ LynxElementProps,
270
+ LynxPressableProps {
271
+ icon?: IconProps["icon"];
272
+ prefixIcon?: PrefixIconProps["icon"];
273
+ suffixIcon?: SuffixIconProps["icon"];
274
+ "aria-label"?: string;
275
+ }
276
+
277
+ export const ActionButton = React.forwardRef<unknown, ActionButtonProps>((props, ref) => {
278
+ const {
279
+ children,
280
+ flexGrow,
281
+ layout,
282
+ icon,
283
+ prefixIcon,
284
+ suffixIcon,
285
+ bindtap,
286
+ "main-thread:bindtap": mainThreadBindtap,
287
+ ...variantAndRest
288
+ } = props;
289
+ const { disabled = false, loading = false } = variantAndRest;
290
+ const isInteractive = !disabled && !loading;
291
+ const isIconOnly = layout === "iconOnly";
292
+ const size = variantAndRest.size;
293
+
294
+ if (process.env.NODE_ENV !== "production" && isIconOnly && !props["aria-label"]) {
295
+ console.warn('ActionButton: `layout="iconOnly"` requires `aria-label` for accessibility.');
296
+ }
297
+
298
+ const { pressed, ...pressTapHandlers } = usePressTap({
299
+ disabled: !isInteractive,
300
+ onTap: bindtap,
301
+ mainThreadOnTap: mainThreadBindtap,
302
+ });
303
+
304
+ return (
305
+ <IconRequired enabled={isIconOnly}>
306
+ <ActionButtonRoot
307
+ {...variantAndRest}
308
+ layout={layout}
309
+ pressed={pressed}
310
+ ref={ref}
311
+ style={flexGrow != null ? { flexGrow: resolveFlexValue(flexGrow) } : undefined}
312
+ {...pressTapHandlers}
313
+ >
314
+ {loading ? (
315
+ <>
316
+ <ActionButtonLoadingIndicator size={size} />
317
+ <ActionButtonLoadingContent
318
+ isIconOnly={isIconOnly}
319
+ icon={icon}
320
+ prefixIcon={prefixIcon}
321
+ suffixIcon={suffixIcon}
322
+ >
323
+ {children}
324
+ </ActionButtonLoadingContent>
325
+ </>
326
+ ) : (
327
+ <ActionButtonContent
328
+ isIconOnly={isIconOnly}
329
+ icon={icon}
330
+ prefixIcon={prefixIcon}
331
+ suffixIcon={suffixIcon}
332
+ >
333
+ {children}
334
+ </ActionButtonContent>
335
+ )}
336
+ </ActionButtonRoot>
337
+ </IconRequired>
338
+ );
339
+ });
340
+ ActionButton.displayName = "ActionButton";
@@ -0,0 +1 @@
1
+ export { ActionButton, type ActionButtonProps } from "./ActionButton";
@@ -0,0 +1,24 @@
1
+ export {
2
+ BottomSheetBackdrop as Backdrop,
3
+ BottomSheetBody as Body,
4
+ BottomSheetContent as Content,
5
+ BottomSheetDescription as Description,
6
+ BottomSheetFooter as Footer,
7
+ BottomSheetHandle as Handle,
8
+ BottomSheetHeader as Header,
9
+ BottomSheetPositioner as Positioner,
10
+ BottomSheetRoot as Root,
11
+ BottomSheetTitle as Title,
12
+ BottomSheetTrigger as Trigger,
13
+ type BottomSheetBackdropProps as BackdropProps,
14
+ type BottomSheetBodyProps as BodyProps,
15
+ type BottomSheetContentProps as ContentProps,
16
+ type BottomSheetDescriptionProps as DescriptionProps,
17
+ type BottomSheetFooterProps as FooterProps,
18
+ type BottomSheetHandleProps as HandleProps,
19
+ type BottomSheetHeaderProps as HeaderProps,
20
+ type BottomSheetPositionerProps as PositionerProps,
21
+ type BottomSheetRootProps as RootProps,
22
+ type BottomSheetTitleProps as TitleProps,
23
+ type BottomSheetTriggerProps as TriggerProps,
24
+ } from "./BottomSheet";
@@ -0,0 +1,153 @@
1
+ import { fireEvent, render } from "@lynx-js/react/testing-library";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ const sheetMocks = vi.hoisted(() => ({
5
+ contentProps: [] as Array<Record<string, unknown>>,
6
+ rootRef: {
7
+ open: vi.fn(),
8
+ },
9
+ }));
10
+
11
+ vi.mock("@lynx-js/lynx-ui-sheet", async () => {
12
+ const React = await vi.importActual<typeof import("@lynx-js/react")>("@lynx-js/react");
13
+
14
+ const SheetRoot = React.forwardRef<unknown, { children?: React.ReactNode }>((props, ref) => {
15
+ React.useImperativeHandle(ref, () => sheetMocks.rootRef);
16
+ return <>{props.children}</>;
17
+ });
18
+ SheetRoot.displayName = "MockSheetRoot";
19
+
20
+ const SheetView = React.forwardRef<unknown, { children?: React.ReactNode }>((props) => {
21
+ return <>{props.children}</>;
22
+ });
23
+ SheetView.displayName = "MockSheetView";
24
+
25
+ const SheetContent = React.forwardRef<
26
+ unknown,
27
+ Record<string, unknown> & { children?: React.ReactNode }
28
+ >((props) => {
29
+ sheetMocks.contentProps.push(props);
30
+ return <>{props["children"]}</>;
31
+ });
32
+ SheetContent.displayName = "MockSheetContent";
33
+
34
+ const passthrough = (displayName: string) => {
35
+ const Component = React.forwardRef<unknown, { children?: React.ReactNode }>((props) => {
36
+ return <>{props.children}</>;
37
+ });
38
+ Component.displayName = displayName;
39
+ return Component;
40
+ };
41
+
42
+ return {
43
+ SheetBackdrop: passthrough("MockSheetBackdrop"),
44
+ SheetContent,
45
+ SheetHandle: passthrough("MockSheetHandle"),
46
+ SheetRoot,
47
+ SheetView,
48
+ };
49
+ });
50
+
51
+ vi.mock("../../hooks/useSafeArea", () => ({
52
+ useSafeArea: () => ({
53
+ safeAreaInsetTop: "62px",
54
+ safeAreaInsetBottom: "34px",
55
+ }),
56
+ }));
57
+
58
+ import * as BottomSheet from "./BottomSheet.namespace";
59
+
60
+ describe("BottomSheet", () => {
61
+ beforeEach(() => {
62
+ sheetMocks.contentProps = [];
63
+ sheetMocks.rootRef.open.mockClear();
64
+ });
65
+
66
+ it("passes safe area bottom to Content inner padding", () => {
67
+ render(
68
+ <BottomSheet.Root>
69
+ <BottomSheet.Content />
70
+ </BottomSheet.Root>,
71
+ );
72
+
73
+ expect(sheetMocks.contentProps.at(-1)).toMatchObject({
74
+ innerStyle: {
75
+ paddingBottom: "34px",
76
+ },
77
+ });
78
+ });
79
+
80
+ it("skips motion-engine animations when Root has skipAnimation", () => {
81
+ render(
82
+ <BottomSheet.Root skipAnimation>
83
+ <BottomSheet.Content />
84
+ </BottomSheet.Root>,
85
+ );
86
+
87
+ expect(sheetMocks.contentProps.at(-1)).toMatchObject({
88
+ snapAnimation: { type: "tween", duration: 0 },
89
+ enterAnimation: { type: "tween", duration: 0 },
90
+ exitAnimation: { type: "tween", duration: 0 },
91
+ });
92
+ });
93
+
94
+ it("preserves user-provided animations when Root has skipAnimation", () => {
95
+ const snapAnimation = { type: "tween", duration: 120 } as const;
96
+ const enterAnimation = { type: "tween", duration: 140 } as const;
97
+ const exitAnimation = { type: "tween", duration: 90 } as const;
98
+
99
+ render(
100
+ <BottomSheet.Root skipAnimation>
101
+ <BottomSheet.Content
102
+ snapAnimation={snapAnimation}
103
+ enterAnimation={enterAnimation}
104
+ exitAnimation={exitAnimation}
105
+ />
106
+ </BottomSheet.Root>,
107
+ );
108
+
109
+ expect(sheetMocks.contentProps.at(-1)).toMatchObject({
110
+ snapAnimation,
111
+ enterAnimation,
112
+ exitAnimation,
113
+ });
114
+ });
115
+
116
+ it("opens with default animation when Root does not skip animation", () => {
117
+ const { getByText } = render(
118
+ <BottomSheet.Root>
119
+ <BottomSheet.Trigger>
120
+ <text>Open sheet</text>
121
+ </BottomSheet.Trigger>
122
+ </BottomSheet.Root>,
123
+ );
124
+
125
+ const trigger = (getByText("Open sheet") as HTMLElement).parentElement;
126
+ if (!trigger) {
127
+ throw new Error("Expected trigger parent element to exist.");
128
+ }
129
+
130
+ fireEvent.tap(trigger);
131
+
132
+ expect(sheetMocks.rootRef.open).toHaveBeenCalledWith();
133
+ });
134
+
135
+ it("opens without animation when Trigger is tapped inside a skipping Root", () => {
136
+ const { getByText } = render(
137
+ <BottomSheet.Root skipAnimation>
138
+ <BottomSheet.Trigger>
139
+ <text>Open sheet</text>
140
+ </BottomSheet.Trigger>
141
+ </BottomSheet.Root>,
142
+ );
143
+
144
+ const trigger = (getByText("Open sheet") as HTMLElement).parentElement;
145
+ if (!trigger) {
146
+ throw new Error("Expected trigger parent element to exist.");
147
+ }
148
+
149
+ fireEvent.tap(trigger);
150
+
151
+ expect(sheetMocks.rootRef.open).toHaveBeenCalledWith({ animate: false });
152
+ });
153
+ });