@seed-design/lynx-react 0.6.2 → 0.7.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 (236) hide show
  1. package/lib/components/Accordion/Accordion.d.ts.map +1 -1
  2. package/lib/components/Accordion/Accordion.jsx +24 -14
  3. package/lib/components/ActionButton/ActionButton.d.ts +1 -1
  4. package/lib/components/ActionButton/ActionButton.d.ts.map +1 -1
  5. package/lib/components/ActionButton/ActionButton.jsx +12 -4
  6. package/lib/components/AppBar/AppBar.d.ts.map +1 -1
  7. package/lib/components/AppBar/AppBar.jsx +9 -8
  8. package/lib/components/Badge/Badge.d.ts.map +1 -1
  9. package/lib/components/Badge/Badge.jsx +2 -1
  10. package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
  11. package/lib/components/BottomSheet/BottomSheet.jsx +9 -9
  12. package/lib/components/Box/Box.d.ts.map +1 -1
  13. package/lib/components/Box/Box.jsx +2 -1
  14. package/lib/components/Callout/Callout.d.ts +2 -2
  15. package/lib/components/Callout/Callout.d.ts.map +1 -1
  16. package/lib/components/Callout/Callout.jsx +20 -10
  17. package/lib/components/Checkbox/Checkbox.d.ts +4 -2
  18. package/lib/components/Checkbox/Checkbox.d.ts.map +1 -1
  19. package/lib/components/Checkbox/Checkbox.jsx +33 -17
  20. package/lib/components/Chip/Chip.d.ts +1 -1
  21. package/lib/components/Chip/Chip.d.ts.map +1 -1
  22. package/lib/components/Chip/Chip.jsx +36 -16
  23. package/lib/components/Count/Count.d.ts +7 -0
  24. package/lib/components/Count/Count.d.ts.map +1 -0
  25. package/lib/components/Count/Count.jsx +19 -0
  26. package/lib/components/Count/index.d.ts +2 -0
  27. package/lib/components/Count/index.d.ts.map +1 -0
  28. package/lib/components/Divider/Divider.d.ts.map +1 -1
  29. package/lib/components/Divider/Divider.jsx +2 -1
  30. package/lib/components/Field/Field.d.ts.map +1 -1
  31. package/lib/components/Field/Field.jsx +11 -15
  32. package/lib/components/Icon/Icon.d.ts.map +1 -1
  33. package/lib/components/Icon/Icon.jsx +4 -3
  34. package/lib/components/InputButton/InputButton.d.ts +40 -0
  35. package/lib/components/InputButton/InputButton.d.ts.map +1 -0
  36. package/lib/components/InputButton/InputButton.jsx +118 -0
  37. package/lib/components/InputButton/InputButton.namespace.d.ts +2 -0
  38. package/lib/components/InputButton/InputButton.namespace.d.ts.map +1 -0
  39. package/lib/components/InputButton/InputButton.namespace.js +16 -0
  40. package/lib/components/InputButton/index.d.ts +3 -0
  41. package/lib/components/InputButton/index.d.ts.map +1 -0
  42. package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.d.ts.map +1 -1
  43. package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.jsx +12 -8
  44. package/lib/components/List/List.d.ts +51 -0
  45. package/lib/components/List/List.d.ts.map +1 -0
  46. package/lib/components/List/List.jsx +176 -0
  47. package/lib/components/List/List.namespace.d.ts +2 -0
  48. package/lib/components/List/List.namespace.d.ts.map +1 -0
  49. package/lib/components/List/List.namespace.js +18 -0
  50. package/lib/components/List/index.d.ts +3 -0
  51. package/lib/components/List/index.d.ts.map +1 -0
  52. package/lib/components/MannerTemp/MannerTemp.d.ts.map +1 -1
  53. package/lib/components/MannerTemp/MannerTemp.jsx +3 -2
  54. package/lib/components/MannerTempBadge/MannerTempBadge.d.ts.map +1 -1
  55. package/lib/components/MannerTempBadge/MannerTempBadge.jsx +2 -1
  56. package/lib/components/Menu/Menu.d.ts +69 -0
  57. package/lib/components/Menu/Menu.d.ts.map +1 -0
  58. package/lib/components/Menu/Menu.jsx +520 -0
  59. package/lib/components/Menu/Menu.namespace.d.ts +2 -0
  60. package/lib/components/Menu/Menu.namespace.d.ts.map +1 -0
  61. package/lib/components/Menu/Menu.namespace.js +21 -0
  62. package/lib/components/Menu/index.d.ts +3 -0
  63. package/lib/components/Menu/index.d.ts.map +1 -0
  64. package/lib/components/NotificationBadge/NotificationBadge.d.ts.map +1 -1
  65. package/lib/components/NotificationBadge/NotificationBadge.jsx +3 -2
  66. package/lib/components/PageBanner/PageBanner.d.ts +5 -5
  67. package/lib/components/PageBanner/PageBanner.d.ts.map +1 -1
  68. package/lib/components/PageBanner/PageBanner.jsx +25 -14
  69. package/lib/components/ProgressCircle/ProgressCircle.d.ts +2 -2
  70. package/lib/components/ProgressCircle/ProgressCircle.d.ts.map +1 -1
  71. package/lib/components/ProgressCircle/ProgressCircle.jsx +4 -3
  72. package/lib/components/RadioGroup/RadioGroup.d.ts +5 -3
  73. package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  74. package/lib/components/RadioGroup/RadioGroup.jsx +22 -9
  75. package/lib/components/ReactionButton/ReactionButton.d.ts +20 -0
  76. package/lib/components/ReactionButton/ReactionButton.d.ts.map +1 -0
  77. package/lib/components/ReactionButton/ReactionButton.jsx +88 -0
  78. package/lib/components/ReactionButton/index.d.ts +2 -0
  79. package/lib/components/ReactionButton/index.d.ts.map +1 -0
  80. package/lib/components/ScaleFeedback/ScaleFeedback.d.ts +11 -0
  81. package/lib/components/ScaleFeedback/ScaleFeedback.d.ts.map +1 -0
  82. package/lib/components/ScaleFeedback/ScaleFeedback.jsx +16 -0
  83. package/lib/components/ScaleFeedback/index.d.ts +2 -0
  84. package/lib/components/ScaleFeedback/index.d.ts.map +1 -0
  85. package/lib/components/SegmentedControl/SegmentedControl.d.ts +1 -0
  86. package/lib/components/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  87. package/lib/components/SegmentedControl/SegmentedControl.jsx +23 -10
  88. package/lib/components/SelectBox/SelectBox.d.ts.map +1 -1
  89. package/lib/components/SelectBox/SelectBox.jsx +23 -13
  90. package/lib/components/Stack/Stack.d.ts.map +1 -1
  91. package/lib/components/Stack/Stack.jsx +2 -1
  92. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts +85 -0
  93. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts.map +1 -0
  94. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.jsx +256 -0
  95. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.d.ts +2 -0
  96. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.d.ts.map +1 -0
  97. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.js +24 -0
  98. package/lib/components/SwipeableMenuSheet/index.d.ts +3 -0
  99. package/lib/components/SwipeableMenuSheet/index.d.ts.map +1 -0
  100. package/lib/components/Switch/Switch.d.ts +27 -3
  101. package/lib/components/Switch/Switch.d.ts.map +1 -1
  102. package/lib/components/Switch/Switch.jsx +22 -9
  103. package/lib/components/Tabs/Tabs.d.ts +4 -2
  104. package/lib/components/Tabs/Tabs.d.ts.map +1 -1
  105. package/lib/components/Tabs/Tabs.jsx +37 -22
  106. package/lib/components/TagGroup/TagGroup.d.ts.map +1 -1
  107. package/lib/components/TagGroup/TagGroup.jsx +4 -3
  108. package/lib/components/Text/Text.d.ts.map +1 -1
  109. package/lib/components/Text/Text.jsx +2 -1
  110. package/lib/components/TextField/TextField.d.ts.map +1 -1
  111. package/lib/components/TextField/TextField.jsx +35 -23
  112. package/lib/components/index.d.ts +7 -0
  113. package/lib/components/index.d.ts.map +1 -1
  114. package/lib/components/private/Positioning.d.ts +69 -0
  115. package/lib/components/private/Positioning.d.ts.map +1 -0
  116. package/lib/components/private/Positioning.js +109 -0
  117. package/lib/components/private/TrackPositioning.d.ts +22 -0
  118. package/lib/components/private/TrackPositioning.d.ts.map +1 -0
  119. package/lib/contexts/ScaleFeedbackContentContext.d.ts +4 -0
  120. package/lib/contexts/ScaleFeedbackContentContext.d.ts.map +1 -0
  121. package/lib/contexts/ScaleFeedbackContentContext.js +6 -0
  122. package/lib/contexts/index.d.ts +2 -0
  123. package/lib/contexts/index.d.ts.map +1 -0
  124. package/lib/hooks/index.d.ts +1 -0
  125. package/lib/hooks/index.d.ts.map +1 -1
  126. package/lib/hooks/useIconColor.d.ts.map +1 -1
  127. package/lib/hooks/useIconColor.js +8 -3
  128. package/lib/hooks/usePressTap.d.ts +4 -3
  129. package/lib/hooks/usePressTap.d.ts.map +1 -1
  130. package/lib/hooks/useScaleFeedback.d.ts +48 -0
  131. package/lib/hooks/useScaleFeedback.d.ts.map +1 -0
  132. package/lib/hooks/useScaleFeedback.js +108 -0
  133. package/lib/index.d.ts +1 -0
  134. package/lib/index.d.ts.map +1 -1
  135. package/lib/index.js +17 -3
  136. package/lib/node_modules/.bun/@floating-ui_core@1.8.0/node_modules/@floating-ui/core/dist/floating-ui.core.js +493 -0
  137. package/lib/node_modules/.bun/@floating-ui_utils@0.2.12/node_modules/@floating-ui/utils/dist/floating-ui.utils.js +114 -0
  138. package/lib/utils/calculate-scale-feedback.d.ts +5 -0
  139. package/lib/utils/calculate-scale-feedback.d.ts.map +1 -0
  140. package/lib/utils/calculate-scale-feedback.js +13 -0
  141. package/lib/utils/compose-event-handlers.d.ts +5 -0
  142. package/lib/utils/compose-event-handlers.d.ts.map +1 -0
  143. package/lib/utils/compose-event-handlers.js +16 -0
  144. package/lib/utils/create-slot-recipe-context.d.ts.map +1 -1
  145. package/lib/utils/create-slot-recipe-context.jsx +6 -16
  146. package/lib/utils/index.d.ts +1 -0
  147. package/lib/utils/index.d.ts.map +1 -1
  148. package/lib/utils/merge-props.d.ts +15 -0
  149. package/lib/utils/merge-props.d.ts.map +1 -0
  150. package/lib/utils/merge-props.js +36 -0
  151. package/lib/utils/merge-refs.d.ts +8 -0
  152. package/lib/utils/merge-refs.d.ts.map +1 -0
  153. package/lib/utils/merge-refs.js +38 -0
  154. package/package.json +6 -5
  155. package/src/components/Accordion/Accordion.tsx +36 -27
  156. package/src/components/ActionButton/ActionButton.tsx +27 -12
  157. package/src/components/AppBar/AppBar.tsx +17 -18
  158. package/src/components/Badge/Badge.tsx +2 -2
  159. package/src/components/BottomSheet/BottomSheet.tsx +9 -17
  160. package/src/components/Box/Box.tsx +2 -2
  161. package/src/components/Callout/Callout.tsx +39 -22
  162. package/src/components/Checkbox/Checkbox.test.tsx +44 -0
  163. package/src/components/Checkbox/Checkbox.tsx +52 -21
  164. package/src/components/Chip/Chip.test.tsx +50 -1
  165. package/src/components/Chip/Chip.tsx +75 -31
  166. package/src/components/Count/Count.tsx +35 -0
  167. package/src/components/Count/index.ts +1 -0
  168. package/src/components/Divider/Divider.tsx +2 -2
  169. package/src/components/Field/Field.tsx +15 -28
  170. package/src/components/Icon/Icon.test.tsx +4 -6
  171. package/src/components/Icon/Icon.tsx +14 -7
  172. package/src/components/InputButton/InputButton.namespace.ts +20 -0
  173. package/src/components/InputButton/InputButton.test.tsx +82 -0
  174. package/src/components/InputButton/InputButton.tsx +295 -0
  175. package/src/components/InputButton/index.ts +2 -0
  176. package/src/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.tsx +16 -21
  177. package/src/components/List/List.namespace.ts +24 -0
  178. package/src/components/List/List.test.tsx +162 -0
  179. package/src/components/List/List.tsx +476 -0
  180. package/src/components/List/index.ts +3 -0
  181. package/src/components/MannerTemp/MannerTemp.tsx +3 -4
  182. package/src/components/MannerTempBadge/MannerTempBadge.tsx +2 -2
  183. package/src/components/Menu/Menu.namespace.ts +26 -0
  184. package/src/components/Menu/Menu.tsx +883 -0
  185. package/src/components/Menu/index.ts +31 -0
  186. package/src/components/NotificationBadge/NotificationBadge.tsx +4 -9
  187. package/src/components/PageBanner/PageBanner.test.tsx +3 -0
  188. package/src/components/PageBanner/PageBanner.tsx +54 -45
  189. package/src/components/ProgressCircle/ProgressCircle.test.tsx +48 -0
  190. package/src/components/ProgressCircle/ProgressCircle.tsx +22 -3
  191. package/src/components/RadioGroup/RadioGroup.test.tsx +87 -0
  192. package/src/components/RadioGroup/RadioGroup.tsx +50 -14
  193. package/src/components/ReactionButton/ReactionButton.test.tsx +122 -0
  194. package/src/components/ReactionButton/ReactionButton.tsx +139 -0
  195. package/src/components/ReactionButton/index.ts +1 -0
  196. package/src/components/ScaleFeedback/ScaleFeedback.test.tsx +41 -0
  197. package/src/components/ScaleFeedback/ScaleFeedback.tsx +32 -0
  198. package/src/components/ScaleFeedback/index.ts +1 -0
  199. package/src/components/SegmentedControl/SegmentedControl.test.tsx +15 -3
  200. package/src/components/SegmentedControl/SegmentedControl.tsx +35 -14
  201. package/src/components/SelectBox/SelectBox.test.tsx +39 -1
  202. package/src/components/SelectBox/SelectBox.tsx +27 -16
  203. package/src/components/Stack/Stack.tsx +2 -2
  204. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.ts +37 -0
  205. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.test.tsx +258 -0
  206. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx +655 -0
  207. package/src/components/SwipeableMenuSheet/index.ts +43 -0
  208. package/src/components/Switch/Switch.test.tsx +57 -0
  209. package/src/components/Switch/Switch.tsx +60 -16
  210. package/src/components/Tabs/Tabs.test.tsx +19 -0
  211. package/src/components/Tabs/Tabs.tsx +72 -40
  212. package/src/components/TagGroup/TagGroup.tsx +4 -6
  213. package/src/components/Text/Text.tsx +2 -2
  214. package/src/components/TextField/TextField.tsx +60 -54
  215. package/src/components/index.ts +7 -0
  216. package/src/components/private/Positioning.test.ts +235 -0
  217. package/src/components/private/Positioning.ts +223 -0
  218. package/src/components/private/TrackPositioning.test.ts +58 -0
  219. package/src/components/private/TrackPositioning.ts +37 -0
  220. package/src/contexts/ScaleFeedbackContentContext.ts +4 -0
  221. package/src/contexts/index.ts +1 -0
  222. package/src/hooks/index.ts +1 -0
  223. package/src/hooks/useIconColor.test.ts +4 -4
  224. package/src/hooks/useIconColor.ts +13 -6
  225. package/src/hooks/usePressTap.ts +4 -3
  226. package/src/hooks/useScaleFeedback.integration.test.tsx +85 -0
  227. package/src/hooks/useScaleFeedback.test.ts +50 -0
  228. package/src/hooks/useScaleFeedback.ts +199 -0
  229. package/src/index.ts +1 -0
  230. package/src/utils/calculate-scale-feedback.ts +12 -0
  231. package/src/utils/compose-event-handlers.ts +16 -0
  232. package/src/utils/create-slot-recipe-context.tsx +6 -7
  233. package/src/utils/index.ts +1 -0
  234. package/src/utils/merge-props.test.tsx +152 -0
  235. package/src/utils/merge-props.ts +62 -0
  236. package/src/utils/merge-refs.ts +44 -0
@@ -0,0 +1,223 @@
1
+ import {
2
+ arrow as arrowMiddleware,
3
+ computePosition as computeFloatingPosition,
4
+ flip,
5
+ limitShift,
6
+ offset,
7
+ shift,
8
+ size,
9
+ type Platform,
10
+ type Rect as FloatingRect,
11
+ } from "@floating-ui/core";
12
+
13
+ export type Side = "top" | "right" | "bottom" | "left";
14
+ export type Placement = Side | `${Side}-${"start" | "end"}`;
15
+
16
+ export interface Rect {
17
+ left: number;
18
+ top: number;
19
+ right: number;
20
+ bottom: number;
21
+ width: number;
22
+ height: number;
23
+ }
24
+
25
+ export interface Position {
26
+ /** 가용 크기는 콘텐츠 크기와 별개이며 0 이상입니다. */
27
+ availableWidth: number;
28
+ availableHeight: number;
29
+ /** 콘텐츠 내부 좌표입니다. 나머지 축은 placement의 반대쪽 가장자리에 붙입니다. */
30
+ arrow?: { left?: number; top?: number; centerOffset: number };
31
+ left: number;
32
+ top: number;
33
+ width: number;
34
+ height: number;
35
+ placement: Placement;
36
+ transformOrigin: string;
37
+ }
38
+
39
+ interface ReferenceElement {
40
+ rect: FloatingRect;
41
+ }
42
+
43
+ interface FloatingElement {
44
+ dimensions: {
45
+ width: number;
46
+ height: number;
47
+ };
48
+ }
49
+
50
+ export interface PositioningOptions {
51
+ placement: Placement;
52
+ gutter: number;
53
+ overflowPadding: number;
54
+ flip?:
55
+ | false
56
+ | {
57
+ fallbackStrategy?: "bestFit" | "initialPlacement";
58
+ fallbackPlacements?: Placement[];
59
+ };
60
+ shift?:
61
+ | false
62
+ | {
63
+ mainAxis?: boolean;
64
+ crossAxis?: boolean;
65
+ /** reference에서 완전히 떨어지지 않도록 이동을 제한합니다. */
66
+ limit?: boolean;
67
+ };
68
+ /** 생략하면 원래 높이를 보존합니다. 너비 제한은 호출자가 적용합니다. */
69
+ size?: {
70
+ order: "beforeFlip" | "afterShift";
71
+ minimumHeight?: number;
72
+ };
73
+ arrow?: {
74
+ /** 회전 가능한 정사각형 화살표 컨테이너의 한 변입니다. */
75
+ size: number;
76
+ /** 컨테이너 크기와 별개인 화살표 끝의 돌출 길이입니다. gutter에 더합니다. */
77
+ tipHeight: number;
78
+ padding: number;
79
+ };
80
+ }
81
+
82
+ export interface ComputePositionOptions extends PositioningOptions {
83
+ reference: Rect;
84
+ boundary: Rect;
85
+ width: number;
86
+ height: number;
87
+ }
88
+
89
+ function getTransformOrigin(placement: Placement) {
90
+ const [side, align] = placement.split("-") as [Side, "start" | "end" | undefined];
91
+ const crossOrigin = align === "start" ? "0%" : align === "end" ? "100%" : "50%";
92
+ return side === "top"
93
+ ? `${crossOrigin} 100%`
94
+ : side === "bottom"
95
+ ? `${crossOrigin} 0%`
96
+ : side === "left"
97
+ ? `100% ${crossOrigin}`
98
+ : `0% ${crossOrigin}`;
99
+ }
100
+
101
+ /**
102
+ * 모든 좌표와 크기는 동일한 화면 기준 논리 px입니다.
103
+ * boundary의 safe area 보정과 결과의 컨테이너 상대 좌표 변환은 호출자 책임입니다.
104
+ *
105
+ * availableWidth로 너비를 제한했다면 줄바꿈 후 실제 높이를 다시 측정해 호출합니다.
106
+ * native 측정·호출 주기·오래된 비동기 결과 폐기는 이 순수 계산에 포함하지 않습니다.
107
+ */
108
+ export async function computePosition({
109
+ reference,
110
+ boundary,
111
+ width,
112
+ height,
113
+ placement,
114
+ gutter,
115
+ overflowPadding,
116
+ flip: flipOptions = {},
117
+ shift: shiftOptions = {},
118
+ size: sizeOptions,
119
+ arrow: arrowOptions,
120
+ }: ComputePositionOptions): Promise<Position> {
121
+ const referenceElement: ReferenceElement = {
122
+ rect: {
123
+ x: reference.left,
124
+ y: reference.top,
125
+ width: reference.width,
126
+ height: reference.height,
127
+ },
128
+ };
129
+ const floatingElement: FloatingElement = {
130
+ dimensions: {
131
+ width: Math.max(0, width),
132
+ height: Math.max(0, height),
133
+ },
134
+ };
135
+ const arrowElement = arrowOptions
136
+ ? { width: arrowOptions.size, height: arrowOptions.size }
137
+ : undefined;
138
+ const clippingRect: FloatingRect = {
139
+ x: boundary.left,
140
+ y: boundary.top,
141
+ width: boundary.width,
142
+ height: boundary.height,
143
+ };
144
+ const maximumHeight = floatingElement.dimensions.height;
145
+ let availableWidth = 0;
146
+ let availableHeight = 0;
147
+ const platform: Platform = {
148
+ getElementRects: () => ({
149
+ reference: referenceElement.rect,
150
+ floating: {
151
+ x: 0,
152
+ y: 0,
153
+ ...floatingElement.dimensions,
154
+ },
155
+ }),
156
+ getClippingRect: () => clippingRect,
157
+ getDimensions: (element) =>
158
+ element === arrowElement && arrowElement ? arrowElement : floatingElement.dimensions,
159
+ isElement: () => true,
160
+ isRTL: () => false,
161
+ };
162
+ const heightMiddleware =
163
+ sizeOptions &&
164
+ size({
165
+ padding: overflowPadding,
166
+ apply({ availableHeight }) {
167
+ floatingElement.dimensions.height = Math.min(
168
+ maximumHeight,
169
+ Math.max(0, sizeOptions.minimumHeight ?? 0, availableHeight),
170
+ );
171
+ },
172
+ });
173
+
174
+ const result = await computeFloatingPosition(referenceElement, floatingElement, {
175
+ placement,
176
+ strategy: "absolute",
177
+ platform,
178
+ middleware: [
179
+ offset(gutter + (arrowOptions?.tipHeight ?? 0)),
180
+ sizeOptions?.order === "beforeFlip" && heightMiddleware,
181
+ flipOptions !== false &&
182
+ flip({
183
+ ...flipOptions,
184
+ padding: overflowPadding,
185
+ }),
186
+ shiftOptions !== false &&
187
+ shift({
188
+ mainAxis: shiftOptions.mainAxis,
189
+ crossAxis: shiftOptions.crossAxis,
190
+ padding: overflowPadding,
191
+ limiter: shiftOptions.limit ? limitShift() : undefined,
192
+ }),
193
+ sizeOptions?.order === "afterShift" && heightMiddleware,
194
+ arrowElement &&
195
+ arrowMiddleware({
196
+ element: arrowElement,
197
+ padding: arrowOptions?.padding,
198
+ }),
199
+ // flip·shift·arrow 보정 후의 제약을 반환하며, 여기서는 크기를 바꾸지 않습니다.
200
+ size({
201
+ padding: overflowPadding,
202
+ apply(available) {
203
+ availableWidth = Math.max(0, available.availableWidth);
204
+ availableHeight = Math.max(0, available.availableHeight);
205
+ },
206
+ }),
207
+ ],
208
+ });
209
+ const resolvedPlacement = result.placement as Placement;
210
+ const arrow = result.middlewareData.arrow;
211
+
212
+ return {
213
+ left: result.x,
214
+ top: result.y,
215
+ width: floatingElement.dimensions.width,
216
+ height: floatingElement.dimensions.height,
217
+ placement: resolvedPlacement,
218
+ transformOrigin: getTransformOrigin(resolvedPlacement),
219
+ availableWidth,
220
+ availableHeight,
221
+ ...(arrow && { arrow: { left: arrow.x, top: arrow.y, centerOffset: arrow.centerOffset } }),
222
+ };
223
+ }
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { getTrackPosition } from "./TrackPositioning";
4
+
5
+ const dimensions = { trackWidth: 300, thumbWidth: 24, labelWidth: 100 };
6
+
7
+ describe("getTrackPosition", () => {
8
+ it("sticks the label to track edges while its arrow follows the thumb", () => {
9
+ expect(getTrackPosition({ ...dimensions, progress: 0 })).toEqual({
10
+ thumbCenter: 12,
11
+ labelLeft: 0,
12
+ arrowCenter: 12,
13
+ });
14
+ expect(getTrackPosition({ ...dimensions, progress: 0.5 })).toEqual({
15
+ thumbCenter: 150,
16
+ labelLeft: 100,
17
+ arrowCenter: 50,
18
+ });
19
+ expect(getTrackPosition({ ...dimensions, progress: 1 })).toEqual({
20
+ thumbCenter: 288,
21
+ labelLeft: 200,
22
+ arrowCenter: 88,
23
+ });
24
+ });
25
+
26
+ it("mirrors both label and arrow in RTL for independently sized indicators", () => {
27
+ const ltr = getTrackPosition({ ...dimensions, progress: 0.2, labelWidth: 160 });
28
+ const rtl = getTrackPosition({
29
+ ...dimensions,
30
+ progress: 0.2,
31
+ labelWidth: 160,
32
+ direction: "rtl",
33
+ });
34
+
35
+ expect(rtl.thumbCenter).toBeCloseTo(300 - ltr.thumbCenter);
36
+ expect(rtl.labelLeft).toBeCloseTo(300 - 160 - ltr.labelLeft);
37
+ expect(rtl.arrowCenter).toBeCloseTo(160 - ltr.arrowCenter);
38
+ expect(rtl.labelLeft + rtl.arrowCenter).toBeCloseTo(rtl.thumbCenter);
39
+ });
40
+
41
+ it("centers an oversized label without losing its arrow target", () => {
42
+ expect(getTrackPosition({ ...dimensions, progress: 0, labelWidth: 400 })).toEqual({
43
+ thumbCenter: 12,
44
+ labelLeft: -50,
45
+ arrowCenter: 62,
46
+ });
47
+ });
48
+
49
+ it("clamps an out-of-range value even when the thumb is wider than the track", () => {
50
+ expect(
51
+ getTrackPosition({ progress: 2, trackWidth: 20, thumbWidth: 24, labelWidth: 10 }),
52
+ ).toEqual({
53
+ thumbCenter: 10,
54
+ labelLeft: 5,
55
+ arrowCenter: 5,
56
+ });
57
+ });
58
+ });
@@ -0,0 +1,37 @@
1
+ export interface TrackPositioningOptions {
2
+ /** 최솟값이 0, 최댓값이 1인 값의 비율입니다. 범위 밖이면 양 끝에 고정합니다. */
3
+ progress: number;
4
+ trackWidth: number;
5
+ thumbWidth: number;
6
+ labelWidth: number;
7
+ direction?: "ltr" | "rtl";
8
+ }
9
+
10
+ /**
11
+ * 트랙 왼쪽 기준 논리 px로 thumb 중심과 label 왼쪽 좌표를 계산합니다.
12
+ * arrowCenter는 label 왼쪽 기준이며 화살표 너비의 절반을 빼서 배치합니다.
13
+ * 트랙보다 넓은 label은 중앙에 두고 양쪽으로 넘치게 합니다.
14
+ *
15
+ * 각 thumb에 대해 호출하며, 측정값을 재사용할 수 있는 동기식 계산입니다.
16
+ * MTS에서는 이 모듈을 runtime: 'shared'로 import하고 native 측정은 별도로 수행합니다.
17
+ */
18
+ export function getTrackPosition({
19
+ progress,
20
+ trackWidth,
21
+ thumbWidth,
22
+ labelWidth,
23
+ direction = "ltr",
24
+ }: TrackPositioningOptions) {
25
+ const track = Math.max(0, trackWidth);
26
+ const thumb = Math.min(track, Math.max(0, thumbWidth));
27
+ const label = Math.max(0, labelWidth);
28
+ const value = Math.max(0, Math.min(1, progress));
29
+ const physicalProgress = direction === "rtl" ? 1 - value : value;
30
+ const thumbCenter = thumb / 2 + physicalProgress * (track - thumb);
31
+ const labelLeft =
32
+ label > track
33
+ ? (track - label) / 2
34
+ : Math.max(0, Math.min(track - label, thumbCenter - label / 2));
35
+
36
+ return { thumbCenter, labelLeft, arrowCenter: thumbCenter - labelLeft };
37
+ }
@@ -0,0 +1,4 @@
1
+ import { createContext, type Context } from "@lynx-js/react";
2
+
3
+ /** @internal Prevents controls inside an animated content layer from binding a second target. */
4
+ export const ScaleFeedbackContentContext: Context<boolean> = createContext(false);
@@ -0,0 +1 @@
1
+ export * from "./ScaleFeedbackContentContext";
@@ -1,5 +1,6 @@
1
1
  export * from "./useControllableState";
2
2
  export * from "./useIconColor";
3
3
  export * from "./usePressTap";
4
+ export * from "./useScaleFeedback";
4
5
  export * from "./useSafeArea";
5
6
  export * from "./useSeedClassName";
@@ -15,7 +15,7 @@ vi.mock("@lynx-js/react", async (importOriginal) => {
15
15
  return {
16
16
  ...actual,
17
17
  runOnMainThread:
18
- (_worklet: (...args: never[]) => unknown) =>
18
+ () =>
19
19
  (...args: unknown[]) => {
20
20
  testState.workletCalls.push(args.length);
21
21
  },
@@ -36,14 +36,14 @@ describe("useIconColor", () => {
36
36
  expect(result.current["main-thread:binduiappear"]).toBeDefined();
37
37
  });
38
38
 
39
- it("schedules tint color synchronization when the system theme changes", () => {
39
+ it("avoids cross-thread calls on mount and resynchronizes after theme changes", () => {
40
40
  const { rerender } = renderHook(() => useIconColor([]));
41
41
 
42
- expect(testState.workletCalls).toEqual([3]);
42
+ expect(testState.workletCalls).toEqual([]);
43
43
 
44
44
  testState.theme = "dark";
45
45
  rerender();
46
46
 
47
- expect(testState.workletCalls).toEqual([3, 1, 3]);
47
+ expect(testState.workletCalls).toEqual([3]);
48
48
  });
49
49
  });
@@ -1,4 +1,4 @@
1
- import { runOnMainThread, useEffect, useGlobalProps, useMainThreadRef } from "@lynx-js/react";
1
+ import { runOnMainThread, useEffect, useGlobalProps, useMainThreadRef, useRef } from "@lynx-js/react";
2
2
  import type { MainThread } from "@lynx-js/types";
3
3
  import type { DependencyList, RefObject } from "@lynx-js/react";
4
4
 
@@ -47,15 +47,16 @@ function scheduleTintColorSync(
47
47
  frameRef.current = 0;
48
48
  }
49
49
 
50
- // ReactLynx는 effect worklet을 native class patch flush보다 먼저 실행할 수 있다.
51
- // 다음 frame에서 class patch가 반영된 computed color를 읽는다.
50
+ // WebLynx는 effect 시점에 class patch가 반영되어 있으므로 즉시 동기화해
51
+ // 불필요한 한 프레임 지연을 없앤다. Native는 patch flush가 늦을 수 있어
52
+ // 다음 frame에 한 번 더 읽는다.
53
+ syncTintColorOnce(targetRef, sourceRef);
54
+
52
55
  if (typeof requestAnimationFrame === "function") {
53
56
  frameRef.current = requestAnimationFrame(() => {
54
57
  frameRef.current = 0;
55
58
  syncTintColorOnce(targetRef, sourceRef);
56
59
  });
57
- } else {
58
- syncTintColorOnce(targetRef, sourceRef);
59
60
  }
60
61
  }
61
62
 
@@ -90,14 +91,20 @@ export function useIconColor(
90
91
  const enabled = options?.enabled ?? true;
91
92
  const frameRef = useMainThreadRef<number>(0);
92
93
  const theme = (useGlobalProps() as { theme?: unknown } | undefined)?.theme;
94
+ const hasMountedRef = useRef(false);
93
95
 
94
96
  function syncOnUiAppear() {
95
97
  "main thread";
96
98
  if (!enabled) return;
97
- syncTintColorOnce(ref, sourceRef);
99
+ scheduleTintColorSync(ref, sourceRef, frameRef);
98
100
  }
99
101
 
100
102
  useEffect(() => {
103
+ if (!hasMountedRef.current) {
104
+ hasMountedRef.current = true;
105
+ return;
106
+ }
107
+
101
108
  if (!enabled) return;
102
109
 
103
110
  runOnMainThread(scheduleTintColorSync)(ref, sourceRef, frameRef);
@@ -5,6 +5,7 @@ import type { BaseTouchEvent, EventHandler, Target } from "@lynx-js/types";
5
5
  import type { LynxTouchProps } from "../types";
6
6
 
7
7
  type TouchHandler = EventHandler<BaseTouchEvent<Target>>;
8
+ type PressStateHandler = TouchHandler & (() => void);
8
9
 
9
10
  export interface UsePressTapOptions {
10
11
  disabled?: boolean;
@@ -15,9 +16,9 @@ export interface UsePressTapOptions {
15
16
  export interface UsePressTapReturn {
16
17
  pressed: boolean;
17
18
  bindtap: TouchHandler;
18
- bindtouchstart: TouchHandler;
19
- bindtouchend: TouchHandler;
20
- bindtouchcancel: TouchHandler;
19
+ bindtouchstart: PressStateHandler;
20
+ bindtouchend: PressStateHandler;
21
+ bindtouchcancel: PressStateHandler;
21
22
  "main-thread:bindtap"?: LynxTouchProps["main-thread:bindtap"];
22
23
  }
23
24
 
@@ -0,0 +1,85 @@
1
+ import { fireEvent, render, waitSchedule } from "@lynx-js/react/testing-library";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { runOnBackground, useMainThreadRef } from "@lynx-js/react";
4
+ import { mergeProps } from "../utils/merge-props";
5
+ import { ActionButton } from "../components/ActionButton";
6
+
7
+ import { useScaleFeedback } from "./useScaleFeedback";
8
+
9
+ function ScaleFeedbackTarget({ onTouchStart }: { onTouchStart: () => void }) {
10
+ const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback({
11
+ onTouchStart,
12
+ });
13
+
14
+ return <view id="spread-target" {...scaleFeedbackTriggerProps} {...scaleFeedbackTargetProps} />;
15
+ }
16
+
17
+ describe("useScaleFeedback Main Thread integration", () => {
18
+ it("keeps ActionButton's pressed state and user Main Thread handler together", async () => {
19
+ function Example({ report }: { report: () => void }) {
20
+ function handleTouch() {
21
+ "main thread";
22
+ runOnBackground(report)();
23
+ }
24
+ return <ActionButton main-thread:bindtouchstart={handleTouch}>Press</ActionButton>;
25
+ }
26
+ const report = vi.fn();
27
+ const { container } = render(<Example report={report} />, {
28
+ enableMainThread: true,
29
+ enableBackgroundThread: true,
30
+ });
31
+ await waitSchedule();
32
+ const target = container.querySelector(".seed-action-button__root")!;
33
+ fireEvent.touchstart(target, {});
34
+ await waitSchedule();
35
+ expect(report).toHaveBeenCalledTimes(1);
36
+ expect(target.className).toContain("pressed_true");
37
+ fireEvent.touchend(target, {});
38
+ await waitSchedule();
39
+ expect(target.className).not.toContain("pressed_true");
40
+ });
41
+
42
+ it("preserves user Main Thread handlers and refs alongside feedback callbacks", async () => {
43
+ function Example({ report }: { report: (value: string) => void }) {
44
+ const userRef = useMainThreadRef(null);
45
+ const feedback = useScaleFeedback({ onTouchStart: () => report("feedback") });
46
+ function userTouch() {
47
+ "main thread";
48
+ runOnBackground(report)(userRef.current ? "user-attached" : "missing-ref");
49
+ }
50
+ return (
51
+ <view
52
+ id="combined"
53
+ {...mergeProps(feedback.scaleFeedbackTriggerProps, feedback.scaleFeedbackTargetProps, {
54
+ "main-thread:ref": userRef,
55
+ "main-thread:bindtouchstart": userTouch,
56
+ })}
57
+ />
58
+ );
59
+ }
60
+ const report = vi.fn();
61
+ const { container } = render(<Example report={report} />, {
62
+ enableMainThread: true,
63
+ enableBackgroundThread: true,
64
+ });
65
+ await waitSchedule();
66
+ fireEvent.touchstart(container.querySelector("#combined")!, {});
67
+ await waitSchedule();
68
+ expect(report.mock.calls).toEqual([["user-attached"], ["feedback"]]);
69
+ });
70
+ it("runs the exact Background Thread callback from its Main Thread handler", async () => {
71
+ const onTouchStart = vi.fn();
72
+ const { container } = render(<ScaleFeedbackTarget onTouchStart={onTouchStart} />, {
73
+ enableMainThread: true,
74
+ enableBackgroundThread: true,
75
+ });
76
+ await waitSchedule();
77
+
78
+ const target = container.querySelector("#spread-target");
79
+ expect(target).not.toBeNull();
80
+ fireEvent.touchstart(target!, {});
81
+ await waitSchedule();
82
+
83
+ expect(onTouchStart).toHaveBeenCalledTimes(1);
84
+ });
85
+ });
@@ -0,0 +1,50 @@
1
+ import { runOnMainThread } from "@lynx-js/react";
2
+ import { renderHook, waitSchedule } from "@lynx-js/react/testing-library";
3
+ import { describe, expect, expectTypeOf, it } from "vitest";
4
+
5
+ import { calculateScaleFeedback, isReducedMotion } from "../utils/calculate-scale-feedback";
6
+ import { useScaleFeedback, type UseScaleFeedbackOptions } from "./useScaleFeedback";
7
+
8
+ describe("useScaleFeedback", () => {
9
+ it("keeps the animation target from being flattened", () => {
10
+ const { result } = renderHook(() => useScaleFeedback());
11
+
12
+ expect(result.current.scaleFeedbackTargetProps.flatten).toBe(false);
13
+ });
14
+
15
+ it("uses exact touch event callbacks", () => {
16
+ expectTypeOf<UseScaleFeedbackOptions>().toMatchTypeOf<{
17
+ onTouchStart?: () => void;
18
+ onTouchEnd?: () => void;
19
+ onTouchCancel?: () => void;
20
+ }>();
21
+ expectTypeOf<UseScaleFeedbackOptions>().not.toHaveProperty("onPressStart");
22
+ expectTypeOf<UseScaleFeedbackOptions>().not.toHaveProperty("onPressEnd");
23
+ expectTypeOf<UseScaleFeedbackOptions>().not.toHaveProperty("onPressCancel");
24
+ });
25
+ });
26
+
27
+ describe("calculateScaleFeedback", () => {
28
+ it.each([
29
+ { width: 32, height: 32, expected: 30 / 32 },
30
+ { width: 120, height: 48, expected: 46 / 48 },
31
+ { width: 343, height: 48, expected: (343 / 4 - 2) / (343 / 4) },
32
+ { width: 22, height: 22, expected: 22 / 24 },
33
+ ])("calculates the scale for $width x $height", async ({ width, height, expected }) => {
34
+ renderHook(() => undefined);
35
+ await waitSchedule();
36
+ const scale = runOnMainThread(calculateScaleFeedback)(width, height);
37
+ await waitSchedule();
38
+ expect(await scale).toBeCloseTo(expected);
39
+ });
40
+ });
41
+
42
+ describe("isReducedMotion", () => {
43
+ it("only disables motion for the exact reduced value", () => {
44
+ expect(isReducedMotion("reduced")).toBe(true);
45
+ expect(isReducedMotion("preferred")).toBe(false);
46
+ expect(isReducedMotion(undefined)).toBe(false);
47
+ expect(isReducedMotion(null)).toBe(false);
48
+ expect(isReducedMotion("unknown")).toBe(false);
49
+ });
50
+ });