@seed-design/lynx-react 0.7.1 → 0.9.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 (265) hide show
  1. package/lib/components/Accordion/Accordion.jsx +2 -2
  2. package/lib/components/ActionButton/ActionButton.jsx +1 -1
  3. package/lib/components/AlertDialog/AlertDialog.d.ts +64 -0
  4. package/lib/components/AlertDialog/AlertDialog.d.ts.map +1 -0
  5. package/lib/components/AlertDialog/AlertDialog.jsx +138 -0
  6. package/lib/components/AlertDialog/AlertDialog.namespace.d.ts +2 -0
  7. package/lib/components/AlertDialog/AlertDialog.namespace.d.ts.map +1 -0
  8. package/lib/components/AlertDialog/AlertDialog.namespace.js +17 -0
  9. package/lib/components/AlertDialog/index.d.ts +3 -0
  10. package/lib/components/AlertDialog/index.d.ts.map +1 -0
  11. package/lib/components/AppBar/AppBar.d.ts +14 -1
  12. package/lib/components/AppBar/AppBar.d.ts.map +1 -1
  13. package/lib/components/AppBar/AppBar.jsx +42 -16
  14. package/lib/components/AppBar/AppBar.namespace.d.ts +1 -1
  15. package/lib/components/AppBar/AppBar.namespace.d.ts.map +1 -1
  16. package/lib/components/AppBar/context.d.ts +2 -1
  17. package/lib/components/AppBar/context.d.ts.map +1 -1
  18. package/lib/components/AppBar/index.d.ts +1 -1
  19. package/lib/components/AppBar/index.d.ts.map +1 -1
  20. package/lib/components/AppBar/useAppBar.d.ts +2 -1
  21. package/lib/components/AppBar/useAppBar.d.ts.map +1 -1
  22. package/lib/components/AppBar/useAppBar.js +17 -15
  23. package/lib/components/AttachmentDisplay/AttachmentDisplay.d.ts +182 -0
  24. package/lib/components/AttachmentDisplay/AttachmentDisplay.d.ts.map +1 -0
  25. package/lib/components/AttachmentDisplay/AttachmentDisplay.jsx +444 -0
  26. package/lib/components/AttachmentDisplay/index.d.ts +2 -0
  27. package/lib/components/AttachmentDisplay/index.d.ts.map +1 -0
  28. package/lib/components/AttachmentInput/AttachmentInput.d.ts +179 -0
  29. package/lib/components/AttachmentInput/AttachmentInput.d.ts.map +1 -0
  30. package/lib/components/AttachmentInput/AttachmentInput.jsx +520 -0
  31. package/lib/components/AttachmentInput/index.d.ts +2 -0
  32. package/lib/components/AttachmentInput/index.d.ts.map +1 -0
  33. package/lib/components/BottomSheet/BottomSheet.d.ts +1 -1
  34. package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
  35. package/lib/components/BottomSheet/BottomSheet.jsx +1 -7
  36. package/lib/components/Box/Box.d.ts +3 -3
  37. package/lib/components/Box/Box.d.ts.map +1 -1
  38. package/lib/components/Callout/Callout.jsx +2 -2
  39. package/lib/components/Checkbox/Checkbox.jsx +2 -2
  40. package/lib/components/Chip/Chip.jsx +2 -2
  41. package/lib/components/ChipTabs/ChipTabs.d.ts +41 -0
  42. package/lib/components/ChipTabs/ChipTabs.d.ts.map +1 -0
  43. package/lib/components/ChipTabs/ChipTabs.jsx +39 -0
  44. package/lib/components/ChipTabs/ChipTabs.namespace.d.ts +2 -0
  45. package/lib/components/ChipTabs/ChipTabs.namespace.d.ts.map +1 -0
  46. package/lib/components/ChipTabs/ChipTabs.namespace.js +13 -0
  47. package/lib/components/ChipTabs/index.d.ts +3 -0
  48. package/lib/components/ChipTabs/index.d.ts.map +1 -0
  49. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts +18 -0
  50. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts.map +1 -0
  51. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.jsx +113 -0
  52. package/lib/components/ContextualFloatingButton/index.d.ts +2 -0
  53. package/lib/components/ContextualFloatingButton/index.d.ts.map +1 -0
  54. package/lib/components/Dialog/Dialog.d.ts +87 -0
  55. package/lib/components/Dialog/Dialog.d.ts.map +1 -0
  56. package/lib/components/Dialog/Dialog.jsx +147 -0
  57. package/lib/components/Dialog/Dialog.namespace.d.ts +2 -0
  58. package/lib/components/Dialog/Dialog.namespace.d.ts.map +1 -0
  59. package/lib/components/Dialog/Dialog.namespace.js +18 -0
  60. package/lib/components/Dialog/index.d.ts +3 -0
  61. package/lib/components/Dialog/index.d.ts.map +1 -0
  62. package/lib/components/FloatingActionButton/FloatingActionButton.d.ts +23 -0
  63. package/lib/components/FloatingActionButton/FloatingActionButton.d.ts.map +1 -0
  64. package/lib/components/FloatingActionButton/FloatingActionButton.jsx +76 -0
  65. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.d.ts +2 -0
  66. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.d.ts.map +1 -0
  67. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.js +10 -0
  68. package/lib/components/FloatingActionButton/index.d.ts +3 -0
  69. package/lib/components/FloatingActionButton/index.d.ts.map +1 -0
  70. package/lib/components/HelpBubble/HelpBubble.d.ts +67 -0
  71. package/lib/components/HelpBubble/HelpBubble.d.ts.map +1 -0
  72. package/lib/components/HelpBubble/HelpBubble.jsx +600 -0
  73. package/lib/components/HelpBubble/HelpBubble.namespace.d.ts +2 -0
  74. package/lib/components/HelpBubble/HelpBubble.namespace.d.ts.map +1 -0
  75. package/lib/components/HelpBubble/HelpBubble.namespace.js +18 -0
  76. package/lib/components/HelpBubble/index.d.ts +3 -0
  77. package/lib/components/HelpBubble/index.d.ts.map +1 -0
  78. package/lib/components/Icon/Icon.d.ts +1 -0
  79. package/lib/components/Icon/Icon.d.ts.map +1 -1
  80. package/lib/components/Icon/Icon.jsx +2 -1
  81. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.d.ts +28 -0
  82. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.d.ts.map +1 -0
  83. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.jsx +37 -0
  84. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.d.ts +2 -0
  85. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.d.ts.map +1 -0
  86. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.js +9 -0
  87. package/lib/components/IdentityPlaceholder/identity-placeholder-business.js +4 -0
  88. package/lib/components/IdentityPlaceholder/identity-placeholder-person.js +4 -0
  89. package/lib/components/IdentityPlaceholder/index.d.ts +3 -0
  90. package/lib/components/IdentityPlaceholder/index.d.ts.map +1 -0
  91. package/lib/components/InputButton/InputButton.d.ts +2 -0
  92. package/lib/components/InputButton/InputButton.d.ts.map +1 -1
  93. package/lib/components/InputButton/InputButton.jsx +40 -8
  94. package/lib/components/List/List.d.ts +1 -1
  95. package/lib/components/List/List.d.ts.map +1 -1
  96. package/lib/components/List/List.jsx +26 -12
  97. package/lib/components/Menu/Menu.d.ts.map +1 -1
  98. package/lib/components/Menu/Menu.jsx +15 -5
  99. package/lib/components/PageBanner/PageBanner.d.ts +3 -0
  100. package/lib/components/PageBanner/PageBanner.d.ts.map +1 -1
  101. package/lib/components/PageBanner/PageBanner.jsx +49 -11
  102. package/lib/components/QuantityPicker/QuantityPicker.d.ts +53 -0
  103. package/lib/components/QuantityPicker/QuantityPicker.d.ts.map +1 -0
  104. package/lib/components/QuantityPicker/QuantityPicker.jsx +282 -0
  105. package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts +2 -0
  106. package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts.map +1 -0
  107. package/lib/components/QuantityPicker/QuantityPicker.namespace.js +11 -0
  108. package/lib/components/QuantityPicker/index.d.ts +3 -0
  109. package/lib/components/QuantityPicker/index.d.ts.map +1 -0
  110. package/lib/components/RadioGroup/RadioGroup.jsx +2 -2
  111. package/lib/components/ReactionButton/ReactionButton.d.ts +1 -1
  112. package/lib/components/ReactionButton/ReactionButton.d.ts.map +1 -1
  113. package/lib/components/ReactionButton/ReactionButton.jsx +22 -13
  114. package/lib/components/ScrollFog/ScrollFog.d.ts +41 -0
  115. package/lib/components/ScrollFog/ScrollFog.d.ts.map +1 -0
  116. package/lib/components/ScrollFog/ScrollFog.jsx +65 -0
  117. package/lib/components/ScrollFog/index.d.ts +2 -0
  118. package/lib/components/ScrollFog/index.d.ts.map +1 -0
  119. package/lib/components/SegmentedControl/SegmentedControl.jsx +2 -2
  120. package/lib/components/Select/Select.d.ts +112 -0
  121. package/lib/components/Select/Select.d.ts.map +1 -0
  122. package/lib/components/Select/Select.jsx +885 -0
  123. package/lib/components/Select/Select.namespace.d.ts +2 -0
  124. package/lib/components/Select/Select.namespace.d.ts.map +1 -0
  125. package/lib/components/Select/Select.namespace.js +23 -0
  126. package/lib/components/Select/index.d.ts +3 -0
  127. package/lib/components/Select/index.d.ts.map +1 -0
  128. package/lib/components/Skeleton/Skeleton.d.ts +15 -0
  129. package/lib/components/Skeleton/Skeleton.d.ts.map +1 -0
  130. package/lib/components/Skeleton/Skeleton.jsx +18 -0
  131. package/lib/components/Skeleton/index.d.ts +2 -0
  132. package/lib/components/Skeleton/index.d.ts.map +1 -0
  133. package/lib/components/Slider/Slider.d.ts +101 -0
  134. package/lib/components/Slider/Slider.d.ts.map +1 -0
  135. package/lib/components/Slider/Slider.jsx +573 -0
  136. package/lib/components/Slider/Slider.namespace.d.ts +2 -0
  137. package/lib/components/Slider/Slider.namespace.d.ts.map +1 -0
  138. package/lib/components/Slider/Slider.namespace.js +19 -0
  139. package/lib/components/Slider/Slider.utils.d.ts +19 -0
  140. package/lib/components/Slider/Slider.utils.d.ts.map +1 -0
  141. package/lib/components/Slider/Slider.utils.js +106 -0
  142. package/lib/components/Slider/index.d.ts +3 -0
  143. package/lib/components/Slider/index.d.ts.map +1 -0
  144. package/lib/components/Stack/Stack.d.ts +7 -9
  145. package/lib/components/Stack/Stack.d.ts.map +1 -1
  146. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts.map +1 -1
  147. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.jsx +22 -8
  148. package/lib/components/Switch/Switch.d.ts.map +1 -1
  149. package/lib/components/Switch/Switch.jsx +5 -3
  150. package/lib/components/Tabs/Tabs.d.ts +34 -2
  151. package/lib/components/Tabs/Tabs.d.ts.map +1 -1
  152. package/lib/components/Tabs/Tabs.jsx +186 -56
  153. package/lib/components/Tabs/Tabs.utils.d.ts +14 -1
  154. package/lib/components/Tabs/Tabs.utils.d.ts.map +1 -1
  155. package/lib/components/Tabs/Tabs.utils.js +24 -3
  156. package/lib/components/Tabs/index.d.ts +1 -1
  157. package/lib/components/Tabs/index.d.ts.map +1 -1
  158. package/lib/components/index.d.ts +14 -0
  159. package/lib/components/index.d.ts.map +1 -1
  160. package/lib/hooks/useControllableState.d.ts +1 -14
  161. package/lib/hooks/useControllableState.d.ts.map +1 -1
  162. package/lib/hooks/usePressTap.d.ts +1 -27
  163. package/lib/hooks/usePressTap.d.ts.map +1 -1
  164. package/lib/hooks/useSafeArea.d.ts +8 -4
  165. package/lib/hooks/useSafeArea.d.ts.map +1 -1
  166. package/lib/hooks/useSafeArea.js +22 -20
  167. package/lib/index.js +29 -6
  168. package/lib/{hooks/useControllableState.js → packages/lynx-react-headless/use-controllable-state/lib/index.js} +2 -3
  169. package/lib/{hooks/usePressTap.js → packages/lynx-react-headless/use-press-tap/lib/index.js} +13 -10
  170. package/lib/types.d.ts +1 -0
  171. package/lib/types.d.ts.map +1 -1
  172. package/lib/utils/styled.d.ts +46 -6
  173. package/lib/utils/styled.d.ts.map +1 -1
  174. package/lib/utils/styled.js +19 -4
  175. package/package.json +9 -5
  176. package/src/components/AlertDialog/AlertDialog.namespace.ts +22 -0
  177. package/src/components/AlertDialog/AlertDialog.test.tsx +226 -0
  178. package/src/components/AlertDialog/AlertDialog.tsx +374 -0
  179. package/src/components/AlertDialog/index.ts +24 -0
  180. package/src/components/AppBar/AppBar.namespace.ts +1 -0
  181. package/src/components/AppBar/AppBar.test.tsx +252 -3
  182. package/src/components/AppBar/AppBar.tsx +108 -25
  183. package/src/components/AppBar/context.ts +3 -1
  184. package/src/components/AppBar/index.ts +1 -0
  185. package/src/components/AppBar/useAppBar.ts +21 -15
  186. package/src/components/AttachmentDisplay/AttachmentDisplay.tsx +886 -0
  187. package/src/components/AttachmentDisplay/index.ts +48 -0
  188. package/src/components/AttachmentInput/AttachmentInput.tsx +1001 -0
  189. package/src/components/AttachmentInput/index.ts +49 -0
  190. package/src/components/BottomSheet/BottomSheet.test.tsx +22 -7
  191. package/src/components/BottomSheet/BottomSheet.tsx +1 -19
  192. package/src/components/Box/Box.test.tsx +104 -0
  193. package/src/components/Box/Box.tsx +9 -6
  194. package/src/components/ChipTabs/ChipTabs.namespace.ts +14 -0
  195. package/src/components/ChipTabs/ChipTabs.tsx +119 -0
  196. package/src/components/ChipTabs/index.ts +16 -0
  197. package/src/components/ContextualFloatingButton/ContextualFloatingButton.test.tsx +63 -0
  198. package/src/components/ContextualFloatingButton/ContextualFloatingButton.tsx +232 -0
  199. package/src/components/ContextualFloatingButton/index.ts +4 -0
  200. package/src/components/Dialog/Dialog.namespace.ts +24 -0
  201. package/src/components/Dialog/Dialog.test.tsx +172 -0
  202. package/src/components/Dialog/Dialog.tsx +373 -0
  203. package/src/components/Dialog/index.ts +26 -0
  204. package/src/components/FloatingActionButton/FloatingActionButton.namespace.ts +8 -0
  205. package/src/components/FloatingActionButton/FloatingActionButton.test.tsx +59 -0
  206. package/src/components/FloatingActionButton/FloatingActionButton.tsx +207 -0
  207. package/src/components/FloatingActionButton/index.ts +10 -0
  208. package/src/components/HelpBubble/HelpBubble.namespace.ts +24 -0
  209. package/src/components/HelpBubble/HelpBubble.tsx +998 -0
  210. package/src/components/HelpBubble/index.ts +26 -0
  211. package/src/components/Icon/Icon.test.tsx +0 -3
  212. package/src/components/Icon/Icon.tsx +11 -1
  213. package/src/components/IdentityPlaceholder/IdentityPlaceholder.namespace.ts +6 -0
  214. package/src/components/IdentityPlaceholder/IdentityPlaceholder.tsx +103 -0
  215. package/src/components/IdentityPlaceholder/assets.d.ts +4 -0
  216. package/src/components/IdentityPlaceholder/identity-placeholder-business.webp +0 -0
  217. package/src/components/IdentityPlaceholder/identity-placeholder-person.webp +0 -0
  218. package/src/components/IdentityPlaceholder/index.ts +8 -0
  219. package/src/components/InputButton/InputButton.test.tsx +128 -1
  220. package/src/components/InputButton/InputButton.tsx +60 -9
  221. package/src/components/List/List.test.tsx +54 -2
  222. package/src/components/List/List.tsx +43 -10
  223. package/src/components/Menu/Menu.tsx +14 -4
  224. package/src/components/Menu/__tests__/Menu.test.tsx +34 -0
  225. package/src/components/PageBanner/PageBanner.test.tsx +110 -3
  226. package/src/components/PageBanner/PageBanner.tsx +83 -19
  227. package/src/components/QuantityPicker/QuantityPicker.namespace.ts +10 -0
  228. package/src/components/QuantityPicker/QuantityPicker.test.tsx +235 -0
  229. package/src/components/QuantityPicker/QuantityPicker.tsx +605 -0
  230. package/src/components/QuantityPicker/index.ts +14 -0
  231. package/src/components/ReactionButton/ReactionButton.feedback.test.tsx +50 -0
  232. package/src/components/ReactionButton/ReactionButton.test.tsx +5 -3
  233. package/src/components/ReactionButton/ReactionButton.tsx +20 -12
  234. package/src/components/ScrollFog/ScrollFog.test.tsx +222 -0
  235. package/src/components/ScrollFog/ScrollFog.tsx +172 -0
  236. package/src/components/ScrollFog/index.ts +1 -0
  237. package/src/components/Select/Select.namespace.ts +34 -0
  238. package/src/components/Select/Select.test.tsx +297 -0
  239. package/src/components/Select/Select.tsx +1483 -0
  240. package/src/components/Select/index.ts +39 -0
  241. package/src/components/Skeleton/Skeleton.test.tsx +64 -0
  242. package/src/components/Skeleton/Skeleton.tsx +45 -0
  243. package/src/components/Skeleton/index.ts +1 -0
  244. package/src/components/Slider/Slider.namespace.ts +26 -0
  245. package/src/components/Slider/Slider.test.tsx +260 -0
  246. package/src/components/Slider/Slider.tsx +897 -0
  247. package/src/components/Slider/Slider.utils.ts +167 -0
  248. package/src/components/Slider/index.ts +30 -0
  249. package/src/components/Stack/Stack.test.tsx +7 -0
  250. package/src/components/Stack/Stack.tsx +11 -10
  251. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.test.tsx +21 -1
  252. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx +30 -7
  253. package/src/components/Switch/Switch.tsx +14 -13
  254. package/src/components/Tabs/Tabs.test.tsx +218 -1
  255. package/src/components/Tabs/Tabs.tsx +346 -75
  256. package/src/components/Tabs/Tabs.utils.ts +51 -1
  257. package/src/components/Tabs/index.ts +17 -1
  258. package/src/components/index.ts +14 -0
  259. package/src/hooks/useControllableState.ts +1 -43
  260. package/src/hooks/usePressTap.test.ts +23 -11
  261. package/src/hooks/usePressTap.ts +1 -60
  262. package/src/hooks/useSafeArea.test.ts +13 -1
  263. package/src/hooks/useSafeArea.ts +25 -23
  264. package/src/types.ts +1 -0
  265. package/src/utils/styled.ts +127 -9
@@ -6,6 +6,8 @@ import { cloneElement, useMemo } from "@lynx-js/react";
6
6
 
7
7
  import { useControllableState } from "../../hooks/useControllableState";
8
8
  import { usePressTap } from "../../hooks/usePressTap";
9
+ import { useScaleFeedback } from "../../hooks/useScaleFeedback";
10
+ import { mergeProps } from "../../utils/merge-props";
9
11
  import type {
10
12
  LynxAccessibilityProps,
11
13
  LynxPressableProps,
@@ -27,7 +29,7 @@ import { ProgressCircleRange, ProgressCircleRoot } from "../ProgressCircle";
27
29
  */
28
30
  export interface ReactionButtonProps
29
31
  extends Omit<ReactionButtonVariantProps, "selected" | "pressed" | "disabled" | "loading">,
30
- LynxStyledElementProps,
32
+ Omit<LynxStyledElementProps, "flatten">,
31
33
  LynxPressableProps,
32
34
  LynxAccessibilityProps {
33
35
  pressed?: boolean;
@@ -51,12 +53,7 @@ export const ReactionButton = React.forwardRef<unknown, ReactionButtonProps>((pr
51
53
  "accessibility-traits": accessibilityTraits,
52
54
  ...nativeProps
53
55
  } = otherProps;
54
- const {
55
- size,
56
- pressed: pressedProp,
57
- disabled = false,
58
- loading = false,
59
- } = variantProps;
56
+ const { size, pressed: pressedProp, disabled = false, loading = false } = variantProps;
60
57
  const [selected, setSelected] = useControllableState({
61
58
  value: pressedProp,
62
59
  defaultValue: defaultPressed,
@@ -69,11 +66,17 @@ export const ReactionButton = React.forwardRef<unknown, ReactionButtonProps>((pr
69
66
  },
70
67
  [bindtap, selected, setSelected],
71
68
  );
72
- const { pressed, ...pressHandlers } = usePressTap({
73
- disabled,
69
+ const { pressed, bindtouchstart, bindtouchend, bindtouchcancel, ...pressHandlers } = usePressTap({
70
+ disabled: disabled || loading,
74
71
  onTap: handleTap,
75
72
  mainThreadOnTap: mainThreadBindtap,
76
73
  });
74
+ const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback({
75
+ disabled: disabled || loading,
76
+ onTouchStart: bindtouchstart,
77
+ onTouchEnd: bindtouchend,
78
+ onTouchCancel: bindtouchcancel,
79
+ });
77
80
  const classNames = reactionButton({ ...variantProps, selected, pressed, disabled, loading });
78
81
  const iconSlotContextValue = useMemo(
79
82
  () => ({
@@ -104,15 +107,20 @@ export const ReactionButton = React.forwardRef<unknown, ReactionButtonProps>((pr
104
107
  return (
105
108
  <IconSlotProvider value={iconSlotContextValue}>
106
109
  <view
107
- {...(ref ? { ref: ref as LynxViewRef } : {})}
108
- {...nativeProps}
110
+ {...mergeProps(
111
+ ref ? { ref: ref as LynxViewRef } : {},
112
+ scaleFeedbackTargetProps,
113
+ scaleFeedbackTriggerProps,
114
+ pressHandlers,
115
+ nativeProps,
116
+ )}
117
+ flatten={false}
109
118
  className={clsx(classNames.root, className)}
110
119
  style={style}
111
120
  accessibility-element={accessibilityElement}
112
121
  accessibility-traits={
113
122
  disabled ? "disabled" : selected ? "selected" : (accessibilityTraits ?? "button")
114
123
  }
115
- {...pressHandlers}
116
124
  >
117
125
  <view className={classNames.content}>
118
126
  {prefixIconChildren}
@@ -0,0 +1,222 @@
1
+ import "@testing-library/jest-dom";
2
+ import { createRef } from "@lynx-js/react";
3
+ import { fireEvent, render } from "@lynx-js/react/testing-library";
4
+ import type { NodesRef } from "@lynx-js/types";
5
+ import { describe, expect, expectTypeOf, it, vi } from "vitest";
6
+
7
+ import { ScrollFog, type ScrollFogProps } from "./ScrollFog";
8
+
9
+ type Edge = "top" | "bottom" | "left" | "right";
10
+
11
+ const EDGES: Edge[] = ["top", "bottom", "left", "right"];
12
+
13
+ function getRoot(container: HTMLElement): HTMLElement {
14
+ const root = container.firstElementChild;
15
+ if (!root || root.tagName.toLowerCase() !== "view") {
16
+ throw new Error("root view가 렌더되어야 합니다.");
17
+ }
18
+ return root as HTMLElement;
19
+ }
20
+
21
+ function getMask(container: HTMLElement, edge: Edge): HTMLElement {
22
+ const mask = container.querySelector<HTMLElement>(`.seed-scroll-fog__${edge}Mask`);
23
+ if (!mask) {
24
+ throw new Error(`${edge} mask view가 렌더되어야 합니다.`);
25
+ }
26
+ return mask;
27
+ }
28
+
29
+ function getMasks(container: HTMLElement): HTMLElement[] {
30
+ return Array.from(
31
+ container.querySelectorAll<HTMLElement>('[class*="seed-scroll-fog__"][class*="Mask"]'),
32
+ );
33
+ }
34
+
35
+ function getScrollViews(container: HTMLElement): [HTMLElement, HTMLElement] {
36
+ const vertical = container.querySelector<HTMLElement>(".seed-scroll-fog__verticalScroll");
37
+ const horizontal = container.querySelector<HTMLElement>(".seed-scroll-fog__horizontalScroll");
38
+ if (!vertical || !horizontal) {
39
+ throw new Error("vertical 및 horizontal scroll-view가 렌더되어야 합니다.");
40
+ }
41
+ return [vertical, horizontal];
42
+ }
43
+
44
+ function expectEdgeVariant(container: HTMLElement, edge: Edge, enabled: boolean): void {
45
+ expect(getMask(container, edge)).toHaveClass(`seed-scroll-fog__${edge}Mask--${edge}_${enabled}`);
46
+ }
47
+
48
+ describe("ScrollFog", () => {
49
+ it("exposes view props without native scroll-view or main-thread props", () => {
50
+ type MainThreadProp = Extract<keyof ScrollFogProps, `main-thread:${string}`>;
51
+ type ScrollViewOnlyProp = Extract<
52
+ keyof ScrollFogProps,
53
+ "bounces" | "bindscroll" | "fading-edge-length" | "scroll-orientation"
54
+ >;
55
+
56
+ expectTypeOf<MainThreadProp>().toEqualTypeOf<never>();
57
+ expectTypeOf<ScrollViewOnlyProp>().toEqualTypeOf<never>();
58
+ expectTypeOf<ScrollFogProps>().toHaveProperty("bindtap");
59
+ });
60
+
61
+ it("renders the default mask and scroll slots without internal inline styles", () => {
62
+ const { container } = render(<ScrollFog />);
63
+ const root = getRoot(container);
64
+ const [vertical, horizontal] = getScrollViews(container);
65
+
66
+ expect(root).toHaveClass("seed-scroll-fog__root", "seed-scroll-fog__root--top_true");
67
+ expect(root).toHaveClass("seed-scroll-fog__root--bottom_true");
68
+ expectEdgeVariant(container, "top", true);
69
+ expectEdgeVariant(container, "bottom", true);
70
+ expect(getMasks(container)).toHaveLength(2);
71
+ expect(vertical).toContainElement(horizontal);
72
+
73
+ for (const element of root.querySelectorAll("view, scroll-view")) {
74
+ expect(element).not.toHaveAttribute("style");
75
+ }
76
+ });
77
+
78
+ it("renders only the selected masks for an arbitrary placement", () => {
79
+ const { container } = render(<ScrollFog placement={["bottom", "left"]} />);
80
+
81
+ expectEdgeVariant(container, "bottom", true);
82
+ expectEdgeVariant(container, "left", true);
83
+ expect(container.querySelector(".seed-scroll-fog__topMask")).not.toBeInTheDocument();
84
+ expect(container.querySelector(".seed-scroll-fog__rightMask")).not.toBeInTheDocument();
85
+ expect(getMasks(container)).toHaveLength(2);
86
+ });
87
+
88
+ it("sets inherited edge-size variables and keeps the truthy zero fallback", () => {
89
+ const { container } = render(
90
+ <ScrollFog size="1.5rem" sizes={{ top: 12, bottom: 0, left: 32, right: 0 }} />,
91
+ );
92
+ const rootStyle = getRoot(container).style as CSSStyleDeclaration &
93
+ Record<`--${string}`, string>;
94
+
95
+ expect(rootStyle["--scroll-fog-size-top"]).toBe("12px");
96
+ expect(rootStyle["--scroll-fog-size-bottom"]).toBe("1.5rem");
97
+ expect(rootStyle["--scroll-fog-size-left"]).toBe("32px");
98
+ expect(rootStyle["--scroll-fog-size-right"]).toBe("1.5rem");
99
+ });
100
+
101
+ it("keeps computed sizes ahead of conflicting object styles", () => {
102
+ const { container } = render(
103
+ <ScrollFog
104
+ size="24px"
105
+ sizes={{ top: 12 }}
106
+ style={
107
+ {
108
+ "--scroll-fog-size-top": "99px",
109
+ "--scroll-fog-size-bottom": "88px",
110
+ height: "100px",
111
+ } as ScrollFogProps["style"]
112
+ }
113
+ />,
114
+ );
115
+ const rootStyle = getRoot(container).style as CSSStyleDeclaration &
116
+ Record<`--${string}`, string>;
117
+
118
+ expect(rootStyle["--scroll-fog-size-top"]).toBe("12px");
119
+ expect(rootStyle["--scroll-fog-size-bottom"]).toBe("24px");
120
+ expect(rootStyle.height).toBe("100px");
121
+ });
122
+
123
+ it("keeps computed sizes ahead of conflicting string styles", () => {
124
+ const { container } = render(
125
+ <ScrollFog
126
+ size="2rem"
127
+ sizes={{ right: 32 }}
128
+ style="--scroll-fog-size-left: 77px; --scroll-fog-size-right: 66px; width: 80px"
129
+ />,
130
+ );
131
+ const rootStyle = getRoot(container).style as CSSStyleDeclaration &
132
+ Record<`--${string}`, string>;
133
+
134
+ expect(rootStyle.getPropertyValue("--scroll-fog-size-left")).toBe("2rem");
135
+ expect(rootStyle.getPropertyValue("--scroll-fog-size-right")).toBe("32px");
136
+ expect(rootStyle.width).toBe("80px");
137
+ });
138
+
139
+ it("keeps two nested scroll axes and controls both native scrollbars", () => {
140
+ const visible = render(<ScrollFog />);
141
+ const [visibleVertical, visibleHorizontal] = getScrollViews(visible.container);
142
+
143
+ expect(visibleVertical).toHaveAttribute("scroll-orientation", "vertical");
144
+ expect(visibleHorizontal).toHaveAttribute("scroll-orientation", "horizontal");
145
+ expect(visibleVertical).toHaveAttribute("enable-nested-scroll", "true");
146
+ expect(visibleHorizontal).toHaveAttribute("enable-nested-scroll", "true");
147
+ expect(visibleVertical).toHaveAttribute("scroll-bar-enable", "true");
148
+ expect(visibleHorizontal).toHaveAttribute("scroll-bar-enable", "true");
149
+ expect(visibleVertical).toContainElement(visibleHorizontal);
150
+ visible.unmount();
151
+
152
+ const hidden = render(<ScrollFog hideScrollBar />);
153
+ for (const scrollView of getScrollViews(hidden.container)) {
154
+ expect(scrollView).toHaveAttribute("scroll-bar-enable", "false");
155
+ }
156
+ });
157
+
158
+ it("forwards root props, merges class and style, and keeps children and ref on the root", () => {
159
+ const bindtap = vi.fn();
160
+ const rootRef = createRef<NodesRef>();
161
+ const { container } = render(
162
+ <ScrollFog
163
+ ref={rootRef}
164
+ id="scroll-fog"
165
+ accessibility-label="Scrollable content"
166
+ className="custom-scroll"
167
+ style={{ height: "100px" }}
168
+ bindtap={bindtap}
169
+ >
170
+ <text>Content</text>
171
+ </ScrollFog>,
172
+ );
173
+
174
+ const root = getRoot(container);
175
+ expect(root).toHaveAttribute("id", "scroll-fog");
176
+ expect(root).toHaveAttribute("accessibility-label", "Scrollable content");
177
+ expect(root).toHaveClass("seed-scroll-fog__root", "custom-scroll");
178
+ expect(root).toHaveStyle({ height: "100px" });
179
+ expect(root.querySelector("text")?.textContent).toBe("Content");
180
+ expect(root.querySelector("scroll-view")).not.toHaveAttribute("id");
181
+ expect(rootRef.current).not.toBeNull();
182
+ expect(Array.from(root.attributes).some(({ name }) => name.startsWith("react-ref-"))).toBe(
183
+ true,
184
+ );
185
+ for (const scrollView of getScrollViews(container)) {
186
+ expect(
187
+ Array.from(scrollView.attributes).some(({ name }) => name.startsWith("react-ref-")),
188
+ ).toBe(false);
189
+ }
190
+
191
+ fireEvent.tap(root);
192
+ expect(bindtap).toHaveBeenCalledTimes(1);
193
+ });
194
+
195
+ it("renders all four masks in canonical order regardless of placement order", () => {
196
+ const { container } = render(<ScrollFog placement={["right", "left", "bottom", "top"]} />);
197
+
198
+ for (const edge of EDGES) {
199
+ expectEdgeVariant(container, edge, true);
200
+ }
201
+ expect(getMasks(container).map(({ classList }) => classList.item(0))).toEqual(
202
+ EDGES.map((edge) => `seed-scroll-fog__${edge}Mask`),
203
+ );
204
+ });
205
+
206
+ it("renders one mask for a single edge", () => {
207
+ const { container } = render(<ScrollFog placement={["right"]} />);
208
+
209
+ expectEdgeVariant(container, "right", true);
210
+ expect(getMasks(container)).toHaveLength(1);
211
+ });
212
+
213
+ it("renders the scroll views without a mask wrapper for an empty placement", () => {
214
+ const { container } = render(<ScrollFog placement={[]} />);
215
+ const root = getRoot(container);
216
+ const [vertical, horizontal] = getScrollViews(container);
217
+
218
+ expect(getMasks(container)).toHaveLength(0);
219
+ expect(root.firstElementChild).toBe(vertical);
220
+ expect(vertical).toContainElement(horizontal);
221
+ });
222
+ });
@@ -0,0 +1,172 @@
1
+ import { scrollFog } from "@seed-design/lynx-css/recipes/scroll-fog";
2
+ import { scrollFog as scrollFogVars } from "@seed-design/lynx-css/vars/component";
3
+ import * as React from "@lynx-js/react";
4
+ import type {
5
+ ForwardRefExoticComponent,
6
+ PropsWithoutRef,
7
+ ReactNode,
8
+ RefAttributes,
9
+ } from "@lynx-js/react";
10
+ import type { IntrinsicElements, NodesRef } from "@lynx-js/types";
11
+ import clsx from "clsx";
12
+
13
+ type NativeViewProps = Omit<IntrinsicElements["view"], `main-thread:${string}`>;
14
+ type LynxForwardRefComponent<T, P> = ForwardRefExoticComponent<
15
+ PropsWithoutRef<P> & RefAttributes<T>
16
+ >;
17
+
18
+ type ScrollFogPlacement = "top" | "bottom" | "left" | "right";
19
+
20
+ type SizesConfig = {
21
+ top?: number;
22
+ bottom?: number;
23
+ left?: number;
24
+ right?: number;
25
+ };
26
+
27
+ const DEFAULT_SIZE = scrollFogVars.base.enabled.root.size;
28
+
29
+ function normalizeSize(size: number | string): string {
30
+ return typeof size === "number" ? `${size}px` : size;
31
+ }
32
+
33
+ function createRootStyle(
34
+ style: NativeViewProps["style"],
35
+ sizes: Record<ScrollFogPlacement, string>,
36
+ ): NativeViewProps["style"] {
37
+ const variables = {
38
+ "--scroll-fog-size-top": sizes.top,
39
+ "--scroll-fog-size-bottom": sizes.bottom,
40
+ "--scroll-fog-size-left": sizes.left,
41
+ "--scroll-fog-size-right": sizes.right,
42
+ };
43
+
44
+ if (typeof style === "string") {
45
+ const variableStyle = Object.entries(variables)
46
+ .map(([property, value]) => `${property}: ${value}`)
47
+ .join("; ");
48
+
49
+ return `${style}; ${variableStyle}`;
50
+ }
51
+
52
+ return { ...style, ...variables } as NativeViewProps["style"];
53
+ }
54
+
55
+ /**
56
+ * @platform Lynx
57
+ *
58
+ * A two-axis scroll area with independently configurable edge masks.
59
+ */
60
+ export interface ScrollFogProps extends Omit<NativeViewProps, "children" | "className" | "style"> {
61
+ children?: ReactNode;
62
+ className?: NativeViewProps["className"];
63
+ style?: NativeViewProps["style"];
64
+ /**
65
+ * Fog 효과를 표시할 방향입니다.
66
+ * @defaultValue ["top", "bottom"]
67
+ */
68
+ placement?: ScrollFogPlacement[];
69
+ /**
70
+ * Fog 효과의 크기입니다. 숫자는 px 단위로 처리합니다.
71
+ * @defaultValue 20
72
+ */
73
+ size?: number | string;
74
+ /** 방향별 Fog 효과의 크기입니다. 숫자는 px 단위로 처리합니다. */
75
+ sizes?: SizesConfig;
76
+ /**
77
+ * Native scroll indicator를 숨깁니다.
78
+ * @defaultValue false
79
+ */
80
+ hideScrollBar?: boolean;
81
+ }
82
+
83
+ export const ScrollFog: LynxForwardRefComponent<NodesRef, ScrollFogProps> = React.forwardRef<
84
+ NodesRef,
85
+ ScrollFogProps
86
+ >((props, forwardedRef) => {
87
+ const {
88
+ children,
89
+ className,
90
+ style,
91
+ placement = ["top", "bottom"],
92
+ size = DEFAULT_SIZE,
93
+ sizes,
94
+ hideScrollBar = false,
95
+ ...rootProps
96
+ } = props;
97
+ const normalizedSize = normalizeSize(size);
98
+ const topSize = sizes?.top ? normalizeSize(sizes.top) : normalizedSize;
99
+ const bottomSize = sizes?.bottom ? normalizeSize(sizes.bottom) : normalizedSize;
100
+ const leftSize = sizes?.left ? normalizeSize(sizes.left) : normalizedSize;
101
+ const rightSize = sizes?.right ? normalizeSize(sizes.right) : normalizedSize;
102
+ const scrollBarEnabled = !hideScrollBar;
103
+ const classNames = scrollFog({
104
+ top: placement.includes("top"),
105
+ bottom: placement.includes("bottom"),
106
+ left: placement.includes("left"),
107
+ right: placement.includes("right"),
108
+ });
109
+ let content: ReactNode = (
110
+ <scroll-view
111
+ enable-nested-scroll
112
+ scroll-bar-enable={scrollBarEnabled}
113
+ scroll-orientation="vertical"
114
+ className={classNames.verticalScroll}
115
+ >
116
+ <scroll-view
117
+ enable-nested-scroll
118
+ scroll-bar-enable={scrollBarEnabled}
119
+ scroll-orientation="horizontal"
120
+ className={classNames.horizontalScroll}
121
+ >
122
+ {children}
123
+ </scroll-view>
124
+ </scroll-view>
125
+ );
126
+
127
+ if (placement.includes("right")) {
128
+ content = (
129
+ <view flatten={false} className={classNames.rightMask}>
130
+ {content}
131
+ </view>
132
+ );
133
+ }
134
+ if (placement.includes("left")) {
135
+ content = (
136
+ <view flatten={false} className={classNames.leftMask}>
137
+ {content}
138
+ </view>
139
+ );
140
+ }
141
+ if (placement.includes("bottom")) {
142
+ content = (
143
+ <view flatten={false} className={classNames.bottomMask}>
144
+ {content}
145
+ </view>
146
+ );
147
+ }
148
+ if (placement.includes("top")) {
149
+ content = (
150
+ <view flatten={false} className={classNames.topMask}>
151
+ {content}
152
+ </view>
153
+ );
154
+ }
155
+
156
+ return (
157
+ <view
158
+ {...(forwardedRef ? ({ ref: forwardedRef } as Record<string, unknown>) : {})}
159
+ {...rootProps}
160
+ className={clsx(classNames.root, className)}
161
+ style={createRootStyle(style, {
162
+ top: topSize,
163
+ bottom: bottomSize,
164
+ left: leftSize,
165
+ right: rightSize,
166
+ })}
167
+ >
168
+ {content}
169
+ </view>
170
+ );
171
+ });
172
+ ScrollFog.displayName = "ScrollFog";
@@ -0,0 +1 @@
1
+ export { ScrollFog, type ScrollFogProps } from "./ScrollFog";
@@ -0,0 +1,34 @@
1
+ export {
2
+ SelectContent as Content,
3
+ SelectGroup as Group,
4
+ SelectGroupLabel as GroupLabel,
5
+ SelectItem as Item,
6
+ SelectItemBody as ItemBody,
7
+ SelectItemDescription as ItemDescription,
8
+ SelectItemIndicator as ItemIndicator,
9
+ SelectItemLabel as ItemLabel,
10
+ SelectItemPrefixIcon as ItemPrefixIcon,
11
+ SelectPlaceholder as Placeholder,
12
+ SelectPrefixIcon as PrefixIcon,
13
+ SelectRoot as Root,
14
+ SelectScrollArea as ScrollArea,
15
+ SelectSuffixIcon as SuffixIcon,
16
+ SelectTrigger as Trigger,
17
+ SelectValue as Value,
18
+ type SelectContentProps as ContentProps,
19
+ type SelectGroupLabelProps as GroupLabelProps,
20
+ type SelectGroupProps as GroupProps,
21
+ type SelectItemBodyProps as ItemBodyProps,
22
+ type SelectItemDescriptionProps as ItemDescriptionProps,
23
+ type SelectItemIndicatorProps as ItemIndicatorProps,
24
+ type SelectItemLabelProps as ItemLabelProps,
25
+ type SelectItemPrefixIconProps as ItemPrefixIconProps,
26
+ type SelectItemProps as ItemProps,
27
+ type SelectPlaceholderProps as PlaceholderProps,
28
+ type SelectPrefixIconProps as PrefixIconProps,
29
+ type SelectRootProps as RootProps,
30
+ type SelectScrollAreaProps as ScrollAreaProps,
31
+ type SelectSuffixIconProps as SuffixIconProps,
32
+ type SelectTriggerProps as TriggerProps,
33
+ type SelectValueProps as ValueProps,
34
+ } from "./Select";