panelui-native 0.71.0 → 0.75.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 (306) hide show
  1. package/README.md +5 -2
  2. package/lib/module/components/accordion/index.js +10 -4
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/area-chart/index.js +1 -0
  5. package/lib/module/components/area-chart/index.js.map +1 -1
  6. package/lib/module/components/avatar/avatar-group.js +64 -0
  7. package/lib/module/components/avatar/avatar-group.js.map +1 -0
  8. package/lib/module/components/avatar/index.js +84 -2
  9. package/lib/module/components/avatar/index.js.map +1 -1
  10. package/lib/module/components/breadcrumb/index.js +2 -1
  11. package/lib/module/components/breadcrumb/index.js.map +1 -1
  12. package/lib/module/components/button/index.js +7 -5
  13. package/lib/module/components/button/index.js.map +1 -1
  14. package/lib/module/components/carousel/index.js +1 -1
  15. package/lib/module/components/carousel/index.js.map +1 -1
  16. package/lib/module/components/chip/index.js +2 -2
  17. package/lib/module/components/chip/index.js.map +1 -1
  18. package/lib/module/components/code-block/index.js.map +1 -1
  19. package/lib/module/components/collapsible/index.js +5 -1
  20. package/lib/module/components/collapsible/index.js.map +1 -1
  21. package/lib/module/components/color-picker/color-picker-accessibility.js +12 -0
  22. package/lib/module/components/color-picker/color-picker-accessibility.js.map +1 -0
  23. package/lib/module/components/color-picker/index.js +9 -5
  24. package/lib/module/components/color-picker/index.js.map +1 -1
  25. package/lib/module/components/date-time-picker/index.js +11 -3
  26. package/lib/module/components/date-time-picker/index.js.map +1 -1
  27. package/lib/module/components/drawer/index.js +58 -13
  28. package/lib/module/components/drawer/index.js.map +1 -1
  29. package/lib/module/components/fab/index.js +7 -6
  30. package/lib/module/components/fab/index.js.map +1 -1
  31. package/lib/module/components/flow/index.js +1 -1
  32. package/lib/module/components/flow/index.js.map +1 -1
  33. package/lib/module/components/frame/index.js +2 -2
  34. package/lib/module/components/frame/index.js.map +1 -1
  35. package/lib/module/components/grid-item/index.js +2 -2
  36. package/lib/module/components/grid-item/index.js.map +1 -1
  37. package/lib/module/components/heatmap-chart/index.js +1 -0
  38. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  39. package/lib/module/components/hex-chart/index.js +1 -0
  40. package/lib/module/components/hex-chart/index.js.map +1 -1
  41. package/lib/module/components/input/index.js +7 -4
  42. package/lib/module/components/input/index.js.map +1 -1
  43. package/lib/module/components/input/input-content-padding.js +6 -0
  44. package/lib/module/components/input/input-content-padding.js.map +1 -0
  45. package/lib/module/components/input-group/index.js +21 -11
  46. package/lib/module/components/input-group/index.js.map +1 -1
  47. package/lib/module/components/input-group/input-group-measurements.js +60 -0
  48. package/lib/module/components/input-group/input-group-measurements.js.map +1 -0
  49. package/lib/module/components/item/index.js +2 -2
  50. package/lib/module/components/item/index.js.map +1 -1
  51. package/lib/module/components/line-chart/index.js +1 -0
  52. package/lib/module/components/line-chart/index.js.map +1 -1
  53. package/lib/module/components/marker/index.js +2 -2
  54. package/lib/module/components/marker/index.js.map +1 -1
  55. package/lib/module/components/marquee/index.js +9 -12
  56. package/lib/module/components/marquee/index.js.map +1 -1
  57. package/lib/module/components/marquee/marquee-math.js +39 -0
  58. package/lib/module/components/marquee/marquee-math.js.map +1 -0
  59. package/lib/module/components/menu/index.js +11 -3
  60. package/lib/module/components/menu/index.js.map +1 -1
  61. package/lib/module/components/otp-input/index.js +3 -1
  62. package/lib/module/components/otp-input/index.js.map +1 -1
  63. package/lib/module/components/otp-input/otp-input-state.js +7 -0
  64. package/lib/module/components/otp-input/otp-input-state.js.map +1 -0
  65. package/lib/module/components/pagination/index.js +27 -11
  66. package/lib/module/components/pagination/index.js.map +1 -1
  67. package/lib/module/components/panelside/index.js +7 -2
  68. package/lib/module/components/panelside/index.js.map +1 -1
  69. package/lib/module/components/planner/index.js +46 -11
  70. package/lib/module/components/planner/index.js.map +1 -1
  71. package/lib/module/components/planner/planner-today.js +8 -0
  72. package/lib/module/components/planner/planner-today.js.map +1 -0
  73. package/lib/module/components/plot/index.js +113 -53
  74. package/lib/module/components/plot/index.js.map +1 -1
  75. package/lib/module/components/plot/plot-series-registry.js +32 -0
  76. package/lib/module/components/plot/plot-series-registry.js.map +1 -0
  77. package/lib/module/components/post/index.js +4 -4
  78. package/lib/module/components/post/index.js.map +1 -1
  79. package/lib/module/components/radio-group/index.js +2 -1
  80. package/lib/module/components/radio-group/index.js.map +1 -1
  81. package/lib/module/components/radio-group/radio-group-value.js +8 -0
  82. package/lib/module/components/radio-group/radio-group-value.js.map +1 -0
  83. package/lib/module/components/rating/index.js +7 -4
  84. package/lib/module/components/rating/index.js.map +1 -1
  85. package/lib/module/components/rating/rating-inputs.js +15 -0
  86. package/lib/module/components/rating/rating-inputs.js.map +1 -0
  87. package/lib/module/components/search-bar/index.js +331 -0
  88. package/lib/module/components/search-bar/index.js.map +1 -0
  89. package/lib/module/components/search-bar/search-bar-debounce.js +23 -0
  90. package/lib/module/components/search-bar/search-bar-debounce.js.map +1 -0
  91. package/lib/module/components/select/index.js +9 -1
  92. package/lib/module/components/select/index.js.map +1 -1
  93. package/lib/module/components/select/native-select-contract.js +11 -0
  94. package/lib/module/components/select/native-select-contract.js.map +1 -0
  95. package/lib/module/components/split-view/index.js +439 -0
  96. package/lib/module/components/split-view/index.js.map +1 -0
  97. package/lib/module/components/split-view/split-view-math.js +132 -0
  98. package/lib/module/components/split-view/split-view-math.js.map +1 -0
  99. package/lib/module/components/splitter/index.js +433 -0
  100. package/lib/module/components/splitter/index.js.map +1 -0
  101. package/lib/module/components/splitter/splitter-math.js +206 -0
  102. package/lib/module/components/splitter/splitter-math.js.map +1 -0
  103. package/lib/module/components/steps/index.js +16 -5
  104. package/lib/module/components/steps/index.js.map +1 -1
  105. package/lib/module/components/steps/steps-trigger.js +6 -0
  106. package/lib/module/components/steps/steps-trigger.js.map +1 -0
  107. package/lib/module/components/table/index.js +3 -4
  108. package/lib/module/components/table/index.js.map +1 -1
  109. package/lib/module/components/tag-input/index.js +15 -10
  110. package/lib/module/components/tag-input/index.js.map +1 -1
  111. package/lib/module/components/tag-input/tag-input-state.js +14 -0
  112. package/lib/module/components/tag-input/tag-input-state.js.map +1 -0
  113. package/lib/module/components/textarea/index.js +9 -4
  114. package/lib/module/components/textarea/index.js.map +1 -1
  115. package/lib/module/components/textarea/textarea-disabled.js +6 -0
  116. package/lib/module/components/textarea/textarea-disabled.js.map +1 -0
  117. package/lib/module/components/timeline/index.js +352 -47
  118. package/lib/module/components/timeline/index.js.map +1 -1
  119. package/lib/module/components/timeline/timeline-geometry.js +11 -0
  120. package/lib/module/components/timeline/timeline-geometry.js.map +1 -0
  121. package/lib/module/components/toast/index.js +18 -1
  122. package/lib/module/components/toast/index.js.map +1 -1
  123. package/lib/module/components/toast/toast-store.js +36 -4
  124. package/lib/module/components/toast/toast-store.js.map +1 -1
  125. package/lib/module/components/toggle-button/index.js +1 -1
  126. package/lib/module/components/toggle-button/index.js.map +1 -1
  127. package/lib/module/components/tour/index.js +4 -2
  128. package/lib/module/components/tour/index.js.map +1 -1
  129. package/lib/module/components/tour/tour-card-measurement.js +24 -0
  130. package/lib/module/components/tour/tour-card-measurement.js.map +1 -0
  131. package/lib/module/components/tree/index.js +13 -8
  132. package/lib/module/components/tree/index.js.map +1 -1
  133. package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
  134. package/lib/module/index.js +3 -0
  135. package/lib/module/index.js.map +1 -1
  136. package/lib/module/utils/chart.js +50 -7
  137. package/lib/module/utils/chart.js.map +1 -1
  138. package/lib/typescript/src/components/accordion/index.d.ts +2 -2
  139. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  140. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  141. package/lib/typescript/src/components/avatar/avatar-group.d.ts +46 -0
  142. package/lib/typescript/src/components/avatar/avatar-group.d.ts.map +1 -0
  143. package/lib/typescript/src/components/avatar/index.d.ts +23 -1
  144. package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
  145. package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -1
  146. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  147. package/lib/typescript/src/components/code-block/index.d.ts +3 -3
  148. package/lib/typescript/src/components/code-block/index.d.ts.map +1 -1
  149. package/lib/typescript/src/components/collapsible/index.d.ts +2 -2
  150. package/lib/typescript/src/components/collapsible/index.d.ts.map +1 -1
  151. package/lib/typescript/src/components/color-picker/color-picker-accessibility.d.ts +8 -0
  152. package/lib/typescript/src/components/color-picker/color-picker-accessibility.d.ts.map +1 -0
  153. package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -1
  154. package/lib/typescript/src/components/date-time-picker/index.d.ts.map +1 -1
  155. package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
  156. package/lib/typescript/src/components/fab/index.d.ts +4 -2
  157. package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
  158. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  159. package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
  160. package/lib/typescript/src/components/input/index.d.ts.map +1 -1
  161. package/lib/typescript/src/components/input/input-content-padding.d.ts +2 -0
  162. package/lib/typescript/src/components/input/input-content-padding.d.ts.map +1 -0
  163. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
  164. package/lib/typescript/src/components/input-group/input-group-measurements.d.ts +16 -0
  165. package/lib/typescript/src/components/input-group/input-group-measurements.d.ts.map +1 -0
  166. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  167. package/lib/typescript/src/components/marquee/index.d.ts +1 -1
  168. package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
  169. package/lib/typescript/src/components/marquee/marquee-math.d.ts +21 -0
  170. package/lib/typescript/src/components/marquee/marquee-math.d.ts.map +1 -0
  171. package/lib/typescript/src/components/menu/index.d.ts +1 -1
  172. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  173. package/lib/typescript/src/components/otp-input/index.d.ts.map +1 -1
  174. package/lib/typescript/src/components/otp-input/otp-input-state.d.ts +3 -0
  175. package/lib/typescript/src/components/otp-input/otp-input-state.d.ts.map +1 -0
  176. package/lib/typescript/src/components/pagination/index.d.ts.map +1 -1
  177. package/lib/typescript/src/components/panelside/index.d.ts +1 -1
  178. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  179. package/lib/typescript/src/components/plan/index.d.ts +2 -2
  180. package/lib/typescript/src/components/planner/index.d.ts.map +1 -1
  181. package/lib/typescript/src/components/planner/planner-today.d.ts +2 -0
  182. package/lib/typescript/src/components/planner/planner-today.d.ts.map +1 -0
  183. package/lib/typescript/src/components/plot/index.d.ts +60 -6
  184. package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
  185. package/lib/typescript/src/components/plot/plot-series-registry.d.ts +8 -0
  186. package/lib/typescript/src/components/plot/plot-series-registry.d.ts.map +1 -0
  187. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  188. package/lib/typescript/src/components/radio-group/radio-group-value.d.ts +2 -0
  189. package/lib/typescript/src/components/radio-group/radio-group-value.d.ts.map +1 -0
  190. package/lib/typescript/src/components/rating/index.d.ts.map +1 -1
  191. package/lib/typescript/src/components/rating/rating-inputs.d.ts +4 -0
  192. package/lib/typescript/src/components/rating/rating-inputs.d.ts.map +1 -0
  193. package/lib/typescript/src/components/search-bar/index.d.ts +216 -0
  194. package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -0
  195. package/lib/typescript/src/components/search-bar/search-bar-debounce.d.ts +7 -0
  196. package/lib/typescript/src/components/search-bar/search-bar-debounce.d.ts.map +1 -0
  197. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  198. package/lib/typescript/src/components/select/native-select-contract.d.ts +11 -0
  199. package/lib/typescript/src/components/select/native-select-contract.d.ts.map +1 -0
  200. package/lib/typescript/src/components/split-view/index.d.ts +159 -0
  201. package/lib/typescript/src/components/split-view/index.d.ts.map +1 -0
  202. package/lib/typescript/src/components/split-view/split-view-math.d.ts +54 -0
  203. package/lib/typescript/src/components/split-view/split-view-math.d.ts.map +1 -0
  204. package/lib/typescript/src/components/splitter/index.d.ts +78 -0
  205. package/lib/typescript/src/components/splitter/index.d.ts.map +1 -0
  206. package/lib/typescript/src/components/splitter/splitter-math.d.ts +67 -0
  207. package/lib/typescript/src/components/splitter/splitter-math.d.ts.map +1 -0
  208. package/lib/typescript/src/components/steps/index.d.ts +2 -2
  209. package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
  210. package/lib/typescript/src/components/steps/steps-trigger.d.ts +2 -0
  211. package/lib/typescript/src/components/steps/steps-trigger.d.ts.map +1 -0
  212. package/lib/typescript/src/components/tag-input/index.d.ts.map +1 -1
  213. package/lib/typescript/src/components/tag-input/tag-input-state.d.ts +12 -0
  214. package/lib/typescript/src/components/tag-input/tag-input-state.d.ts.map +1 -0
  215. package/lib/typescript/src/components/textarea/index.d.ts.map +1 -1
  216. package/lib/typescript/src/components/textarea/textarea-disabled.d.ts +2 -0
  217. package/lib/typescript/src/components/textarea/textarea-disabled.d.ts.map +1 -0
  218. package/lib/typescript/src/components/timeline/index.d.ts +36 -2
  219. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  220. package/lib/typescript/src/components/timeline/timeline-geometry.d.ts +4 -0
  221. package/lib/typescript/src/components/timeline/timeline-geometry.d.ts.map +1 -0
  222. package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
  223. package/lib/typescript/src/components/toast/toast-store.d.ts +7 -2
  224. package/lib/typescript/src/components/toast/toast-store.d.ts.map +1 -1
  225. package/lib/typescript/src/components/tour/index.d.ts.map +1 -1
  226. package/lib/typescript/src/components/tour/tour-card-measurement.d.ts +14 -0
  227. package/lib/typescript/src/components/tour/tour-card-measurement.d.ts.map +1 -0
  228. package/lib/typescript/src/components/tree/index.d.ts +3 -4
  229. package/lib/typescript/src/components/tree/index.d.ts.map +1 -1
  230. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +2 -2
  231. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
  232. package/lib/typescript/src/index.d.ts +5 -2
  233. package/lib/typescript/src/index.d.ts.map +1 -1
  234. package/lib/typescript/src/utils/chart.d.ts +28 -3
  235. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  236. package/package.json +1 -1
  237. package/src/components/accordion/index.tsx +17 -7
  238. package/src/components/area-chart/index.tsx +8 -0
  239. package/src/components/avatar/avatar-group.ts +78 -0
  240. package/src/components/avatar/index.tsx +121 -1
  241. package/src/components/breadcrumb/index.tsx +2 -1
  242. package/src/components/button/index.tsx +3 -2
  243. package/src/components/carousel/index.tsx +1 -1
  244. package/src/components/chip/index.tsx +2 -2
  245. package/src/components/code-block/index.tsx +3 -3
  246. package/src/components/collapsible/index.tsx +13 -4
  247. package/src/components/color-picker/color-picker-accessibility.ts +12 -0
  248. package/src/components/color-picker/index.tsx +30 -10
  249. package/src/components/date-time-picker/index.tsx +11 -3
  250. package/src/components/drawer/index.tsx +42 -14
  251. package/src/components/fab/index.tsx +21 -11
  252. package/src/components/flow/index.tsx +1 -1
  253. package/src/components/frame/index.tsx +2 -2
  254. package/src/components/grid-item/index.tsx +2 -2
  255. package/src/components/heatmap-chart/index.tsx +7 -1
  256. package/src/components/hex-chart/index.tsx +7 -1
  257. package/src/components/input/index.tsx +6 -2
  258. package/src/components/input/input-content-padding.ts +3 -0
  259. package/src/components/input-group/index.tsx +45 -12
  260. package/src/components/input-group/input-group-measurements.ts +98 -0
  261. package/src/components/item/index.tsx +2 -2
  262. package/src/components/line-chart/index.tsx +9 -0
  263. package/src/components/marker/index.tsx +2 -2
  264. package/src/components/marquee/index.tsx +14 -12
  265. package/src/components/marquee/marquee-math.ts +37 -0
  266. package/src/components/menu/index.tsx +11 -3
  267. package/src/components/otp-input/index.tsx +3 -1
  268. package/src/components/otp-input/otp-input-state.ts +7 -0
  269. package/src/components/pagination/index.tsx +26 -13
  270. package/src/components/panelside/index.tsx +7 -2
  271. package/src/components/plan/index.tsx +2 -2
  272. package/src/components/planner/index.tsx +44 -11
  273. package/src/components/planner/planner-today.ts +5 -0
  274. package/src/components/plot/index.tsx +199 -51
  275. package/src/components/plot/plot-series-registry.ts +41 -0
  276. package/src/components/post/index.tsx +4 -4
  277. package/src/components/radio-group/index.tsx +4 -1
  278. package/src/components/radio-group/radio-group-value.ts +9 -0
  279. package/src/components/rating/index.tsx +11 -4
  280. package/src/components/rating/rating-inputs.ts +19 -0
  281. package/src/components/search-bar/index.tsx +441 -0
  282. package/src/components/search-bar/search-bar-debounce.ts +35 -0
  283. package/src/components/select/index.tsx +10 -1
  284. package/src/components/select/native-select-contract.ts +12 -0
  285. package/src/components/split-view/index.tsx +565 -0
  286. package/src/components/split-view/split-view-math.ts +146 -0
  287. package/src/components/splitter/index.tsx +589 -0
  288. package/src/components/splitter/splitter-math.ts +244 -0
  289. package/src/components/steps/index.tsx +35 -9
  290. package/src/components/steps/steps-trigger.ts +6 -0
  291. package/src/components/table/index.tsx +2 -2
  292. package/src/components/tag-input/index.tsx +13 -11
  293. package/src/components/tag-input/tag-input-state.ts +18 -0
  294. package/src/components/textarea/index.tsx +8 -4
  295. package/src/components/textarea/textarea-disabled.ts +6 -0
  296. package/src/components/timeline/index.tsx +405 -46
  297. package/src/components/timeline/timeline-geometry.ts +9 -0
  298. package/src/components/toast/index.tsx +26 -1
  299. package/src/components/toast/toast-store.ts +43 -8
  300. package/src/components/toggle-button/index.tsx +1 -1
  301. package/src/components/tour/index.tsx +11 -3
  302. package/src/components/tour/tour-card-measurement.ts +29 -0
  303. package/src/components/tree/index.tsx +17 -12
  304. package/src/hooks/use-keyboard-avoidance.ts +7 -2
  305. package/src/index.ts +19 -0
  306. package/src/utils/chart.ts +66 -7
@@ -0,0 +1,441 @@
1
+ /**
2
+ * SearchBar — a text field for querying a list, with the two controls a search
3
+ * needs and an ordinary field does not.
4
+ *
5
+ * ```tsx
6
+ * <SearchBar placeholder="Search orders" onSubmit={run} />
7
+ * <SearchBar variant="filled" shape="pill" cancel="focus" />
8
+ * <SearchBar debounce={250} onDebouncedChange={filter} loading={pending} />
9
+ * ```
10
+ *
11
+ * ## The clear button, and why it is not the platform's
12
+ *
13
+ * A ✕ appears inside the field as soon as there is something to clear, and
14
+ * takes it back to empty without dismissing the keyboard — clearing a query is
15
+ * the start of the next one, not the end of the search. It is drawn here
16
+ * rather than left to `clearButtonMode`, which exists on iOS only, cannot be
17
+ * labelled for a screen reader and cannot be swapped for a spinner while
18
+ * results are in flight.
19
+ *
20
+ * The glyph is 24 points and its touch box is 48, made up with slop rather
21
+ * than with size. A 48-point circle inside a 40-point field either overflows
22
+ * it or forces every search bar in an app to be as tall as the largest one.
23
+ *
24
+ * ## Cancel is a row, not a decoration
25
+ *
26
+ * `cancel="focus"` puts a Cancel button beside the field and slides it in
27
+ * while the field is being edited, which is the platform's own answer to
28
+ * "how do I get out of this search". It is a sibling of the field rather than
29
+ * something inside it, because it acts on the search as a whole: it empties
30
+ * the query, drops focus and calls `onCancel`, and a control that ends the
31
+ * thing it sits inside reads as part of the query it is about to discard.
32
+ *
33
+ * Its width is measured once and animated on the UI thread. The button is
34
+ * always mounted when `cancel` is not `never`, so the measurement is already
35
+ * there the first time the field is touched and the first slide is as smooth
36
+ * as the tenth.
37
+ *
38
+ * ## Debouncing belongs to the caller's search, not to the field
39
+ *
40
+ * `onChangeText` always fires on every keystroke — a controlled field that
41
+ * lags its own input is unusable. `debounce` is about the *query*: it holds
42
+ * `onDebouncedChange` until typing pauses, so a network search runs once per
43
+ * pause instead of once per letter. Submitting flushes it immediately, since
44
+ * a return key is somebody saying they are done waiting.
45
+ */
46
+ import {
47
+ forwardRef,
48
+ useCallback,
49
+ useEffect,
50
+ useImperativeHandle,
51
+ useRef,
52
+ useState,
53
+ type ReactNode,
54
+ } from 'react';
55
+ import {
56
+ View,
57
+ type LayoutChangeEvent,
58
+ type NativeSyntheticEvent,
59
+ type TextInput,
60
+ type TextInputSubmitEditingEventData,
61
+ } from 'react-native';
62
+ import Animated, {
63
+ useAnimatedStyle,
64
+ useSharedValue,
65
+ withTiming,
66
+ } from 'react-native-reanimated';
67
+ import { tv, type VariantProps } from 'tailwind-variants';
68
+ import { SearchIcon, XIcon } from '../../icons';
69
+ import { AnimatedPressable } from '../../primitives/animated-pressable';
70
+ import { Text } from '../../primitives/text';
71
+ import { Input, type InputProps } from '../input';
72
+ import { Spinner } from '../spinner';
73
+ import {
74
+ cancelSearchBarDebounce,
75
+ flushSearchBarDebounce,
76
+ scheduleSearchBarDebounce,
77
+ type SearchBarDebounceTimer,
78
+ } from './search-bar-debounce';
79
+
80
+ /** Matches the field's own focus crossfade, so the row settles as one thing. */
81
+ const CANCEL_DURATION = 180;
82
+
83
+ /** Space kept between the field and the Cancel button while it is out. */
84
+ const CANCEL_GAP = 8;
85
+
86
+ /**
87
+ * Slop around the 24-point clear glyph, taking its touch box to 48 without
88
+ * changing the height of the field it sits in.
89
+ */
90
+ const CLEAR_HIT_SLOP = 12;
91
+
92
+ const searchBarVariants = tv({
93
+ slots: {
94
+ row: 'w-full flex-row items-center',
95
+ field: '',
96
+ // Clipped, because this is what the Cancel button is revealed out of: the
97
+ // button keeps its measured width and the container's grows past it.
98
+ //
99
+ // `self-stretch` gives it the row's height, which it has no other way of
100
+ // getting — its only child is positioned absolutely, so it has no content
101
+ // to be as tall as.
102
+ cancelClip: 'self-stretch overflow-hidden',
103
+ cancelButton: 'absolute bottom-0 end-0 top-0 items-center justify-center ps-2',
104
+ cancelLabel: 'font-medium text-primary',
105
+ clear: 'items-center justify-center rounded-full',
106
+ },
107
+ variants: {
108
+ size: {
109
+ sm: { cancelLabel: 'text-[14px]', clear: 'h-6 w-6' },
110
+ md: { cancelLabel: 'text-[16px]', clear: 'h-6 w-6' },
111
+ lg: { cancelLabel: 'text-[16px]', clear: 'h-7 w-7' },
112
+ },
113
+ /**
114
+ * The field's corner. `pill` is the shape a search field takes when it is
115
+ * chrome — sitting above a list, in a header — and `rounded` the one it
116
+ * takes inside a form beside other fields.
117
+ */
118
+ shape: {
119
+ rounded: { field: '' },
120
+ pill: { field: 'rounded-full' },
121
+ },
122
+ },
123
+ defaultVariants: {
124
+ size: 'md',
125
+ shape: 'rounded',
126
+ },
127
+ });
128
+
129
+ type SearchBarVariantProps = VariantProps<typeof searchBarVariants>;
130
+
131
+ /** Glyph sizes per field size — the icon tracks the text, not the box. */
132
+ const ICON_SIZE = { sm: 16, md: 18, lg: 20 } as const;
133
+
134
+ /**
135
+ * What SearchBar takes from Input, minus everything it owns itself. The form
136
+ * furniture is dropped along with it: a label and an error line stack above
137
+ * and below the field, and Cancel sits beside the whole stack rather than
138
+ * beside the field it belongs to. Use `Field` for a search that is one answer
139
+ * in a form.
140
+ */
141
+ type InheritedInputProps = Omit<
142
+ InputProps,
143
+ | 'defaultValue'
144
+ | 'description'
145
+ | 'endContent'
146
+ | 'errorMessage'
147
+ | 'interactiveContent'
148
+ | 'isRequired'
149
+ | 'label'
150
+ | 'multiline'
151
+ | 'onChangeText'
152
+ | 'size'
153
+ | 'startContent'
154
+ | 'value'
155
+ >;
156
+
157
+ export interface SearchBarProps extends InheritedInputProps, SearchBarVariantProps {
158
+ /**
159
+ * The field's background, from `Input`. `outline` draws its own edge, for a
160
+ * search bar sitting on the page; `filled` drops it, for one inside a card
161
+ * or a header where a second border reads as a seam. Defaults to `outline`.
162
+ */
163
+ variant?: InputProps['variant'];
164
+ /** The query, when the caller holds it. Leave unset to let the field keep it. */
165
+ value?: string;
166
+ /** Starting query for an uncontrolled field. Ignored once `value` is passed. */
167
+ defaultValue?: string;
168
+ /** Fires on every keystroke. For a search that costs something, see `debounce`. */
169
+ onChangeText?: (value: string) => void;
170
+ /** The return key, which is labelled Search. Flushes `onDebouncedChange` first. */
171
+ onSubmit?: (value: string) => void;
172
+ /**
173
+ * How long typing has to pause before `onDebouncedChange` runs, in
174
+ * milliseconds. `0` runs it on every keystroke, which is only right for a
175
+ * filter over a list already in memory.
176
+ */
177
+ debounce?: number;
178
+ /** The query, once typing has paused for `debounce` milliseconds. */
179
+ onDebouncedChange?: (value: string) => void;
180
+ /** Fires after the ✕ empties the field. The field keeps focus. */
181
+ onClear?: () => void;
182
+ /** Fires after Cancel empties the field and drops focus. */
183
+ onCancel?: () => void;
184
+ /** Whether the ✕ appears once there is a query. */
185
+ isClearable?: boolean;
186
+ /**
187
+ * When the Cancel button is beside the field. `focus` slides it in while the
188
+ * field is being edited and away again when it is not, which is what a
189
+ * search bar above a list wants. `always` keeps it out, for a screen that is
190
+ * nothing but the search.
191
+ */
192
+ cancel?: 'never' | 'focus' | 'always';
193
+ /** The Cancel button's word. */
194
+ cancelLabel?: string;
195
+ /** How the ✕ announces itself. */
196
+ clearLabel?: string;
197
+ /**
198
+ * Results are on their way. A spinner takes the ✕'s place, because the two
199
+ * would otherwise sit on top of one another at exactly the moment a query is
200
+ * both non-empty and running.
201
+ */
202
+ loading?: boolean;
203
+ /** The leading glyph, for a search over something with a symbol of its own. */
204
+ icon?: ReactNode;
205
+ }
206
+
207
+ export const SearchBar = forwardRef<TextInput, SearchBarProps>(
208
+ (
209
+ {
210
+ value: valueProp,
211
+ defaultValue,
212
+ onChangeText,
213
+ onSubmit,
214
+ debounce = 0,
215
+ onDebouncedChange,
216
+ onClear,
217
+ onCancel,
218
+ isClearable = true,
219
+ cancel = 'never',
220
+ cancelLabel = 'Cancel',
221
+ clearLabel = 'Clear search',
222
+ loading = false,
223
+ icon,
224
+ shape,
225
+ size = 'md',
226
+ className,
227
+ containerClassName,
228
+ disabled,
229
+ onFocus,
230
+ onBlur,
231
+ onSubmitEditing,
232
+ ...props
233
+ },
234
+ ref
235
+ ) => {
236
+ const controlled = valueProp !== undefined;
237
+ const [internal, setInternal] = useState(defaultValue ?? '');
238
+ const text = controlled ? valueProp : internal;
239
+
240
+ const [focused, setFocused] = useState(false);
241
+ const inputRef = useRef<TextInput | null>(null);
242
+ useImperativeHandle(ref, () => inputRef.current as TextInput, []);
243
+
244
+ const slots = searchBarVariants({ size, shape });
245
+
246
+ const setText = useCallback(
247
+ (next: string) => {
248
+ if (!controlled) setInternal(next);
249
+ onChangeText?.(next);
250
+ },
251
+ [controlled, onChangeText]
252
+ );
253
+
254
+ /*
255
+ * The debounced callback is read through a ref so an inline arrow function
256
+ * — which is what a caller writes — does not restart the timer on every
257
+ * render and push the pause out forever.
258
+ */
259
+ const debouncedRef = useRef(onDebouncedChange);
260
+ const debounceTimerRef = useRef<SearchBarDebounceTimer['current']>(null);
261
+ useEffect(() => {
262
+ debouncedRef.current = onDebouncedChange;
263
+ });
264
+
265
+ // Skipped on mount: nothing was typed, so there is no pause to be at the
266
+ // end of, and firing here would run a search for the initial value.
267
+ const settled = useRef(false);
268
+ useEffect(() => {
269
+ if (!settled.current) {
270
+ settled.current = true;
271
+ return;
272
+ }
273
+ scheduleSearchBarDebounce(debounceTimerRef, debouncedRef.current, text, debounce);
274
+ return () => {
275
+ cancelSearchBarDebounce(debounceTimerRef);
276
+ };
277
+ }, [text, debounce]);
278
+
279
+ const handleFocus = useCallback<NonNullable<InputProps['onFocus']>>(
280
+ (event) => {
281
+ setFocused(true);
282
+ onFocus?.(event);
283
+ },
284
+ [onFocus]
285
+ );
286
+
287
+ const handleBlur = useCallback<NonNullable<InputProps['onBlur']>>(
288
+ (event) => {
289
+ setFocused(false);
290
+ onBlur?.(event);
291
+ },
292
+ [onBlur]
293
+ );
294
+
295
+ const handleSubmit = useCallback(
296
+ (event: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
297
+ // A return key is somebody saying they are done waiting, so the
298
+ // pending pause is spent rather than waited out.
299
+ flushSearchBarDebounce(debounceTimerRef, debouncedRef.current, text);
300
+ onSubmit?.(text);
301
+ onSubmitEditing?.(event);
302
+ },
303
+ [onSubmit, onSubmitEditing, text]
304
+ );
305
+
306
+ const handleClear = useCallback(() => {
307
+ setText('');
308
+ onClear?.();
309
+ // The keyboard stays: clearing a query is the start of the next one.
310
+ // Android takes focus away with the press, so it is asked back.
311
+ inputRef.current?.focus();
312
+ }, [onClear, setText]);
313
+
314
+ const handleCancel = useCallback(() => {
315
+ setText('');
316
+ inputRef.current?.blur();
317
+ onCancel?.();
318
+ }, [onCancel, setText]);
319
+
320
+ /*
321
+ * Cancel's width is measured once and driven from a shared value, so the
322
+ * slide costs no re-render. `cancelWidth` is a shared value rather than
323
+ * state for the same reason — a measurement that arrives as state would
324
+ * re-render the field it is beside.
325
+ */
326
+ const cancelWidth = useSharedValue(0);
327
+ const cancelOut = cancel === 'always' || (cancel === 'focus' && focused);
328
+ const cancelProgress = useSharedValue(cancel === 'always' ? 1 : 0);
329
+
330
+ useEffect(() => {
331
+ if (cancel === 'never') return;
332
+ cancelProgress.value = withTiming(cancelOut ? 1 : 0, {
333
+ duration: CANCEL_DURATION,
334
+ });
335
+ }, [cancel, cancelOut, cancelProgress]);
336
+
337
+ const cancelStyle = useAnimatedStyle(() => ({
338
+ width: (cancelWidth.value + CANCEL_GAP) * cancelProgress.value,
339
+ opacity: cancelProgress.value,
340
+ }));
341
+
342
+ const handleCancelLayout = useCallback(
343
+ (event: LayoutChangeEvent) => {
344
+ cancelWidth.value = event.nativeEvent.layout.width;
345
+ },
346
+ [cancelWidth]
347
+ );
348
+
349
+ const startContent = (
350
+ /*
351
+ * Decorative, and said so here rather than through Input's
352
+ * `interactiveContent` — that flag covers both ends of the field, and
353
+ * turning it off to quieten the magnifier would take the clear button's
354
+ * touches with it.
355
+ */
356
+ <View
357
+ pointerEvents="none"
358
+ accessibilityElementsHidden
359
+ importantForAccessibility="no-hide-descendants"
360
+ >
361
+ {icon ?? <SearchIcon size={ICON_SIZE[size]} />}
362
+ </View>
363
+ );
364
+
365
+ const endContent = loading ? (
366
+ <Spinner size="sm" />
367
+ ) : isClearable && text.length > 0 ? (
368
+ <AnimatedPressable
369
+ onPress={handleClear}
370
+ disabled={disabled}
371
+ hitSlop={CLEAR_HIT_SLOP}
372
+ accessibilityRole="button"
373
+ accessibilityLabel={clearLabel}
374
+ className={slots.clear()}
375
+ >
376
+ <XIcon size={ICON_SIZE[size]} />
377
+ </AnimatedPressable>
378
+ ) : null;
379
+
380
+ const field = (
381
+ <Input
382
+ ref={inputRef}
383
+ size={size}
384
+ disabled={disabled}
385
+ className={slots.field({ className })}
386
+ containerClassName={cancel === 'never' ? containerClassName : 'flex-1'}
387
+ startContent={startContent}
388
+ endContent={endContent}
389
+ value={text}
390
+ onChangeText={setText}
391
+ onFocus={handleFocus}
392
+ onBlur={handleBlur}
393
+ onSubmitEditing={handleSubmit}
394
+ accessibilityRole="search"
395
+ returnKeyType="search"
396
+ // The platform's own ✕ would sit on top of this component's, and only
397
+ // on iOS.
398
+ clearButtonMode="never"
399
+ autoCapitalize="none"
400
+ autoCorrect={false}
401
+ spellCheck={false}
402
+ placeholder="Search"
403
+ {...props}
404
+ />
405
+ );
406
+
407
+ if (cancel === 'never') return field;
408
+
409
+ return (
410
+ <View className={slots.row({ className: containerClassName })}>
411
+ {field}
412
+ <Animated.View
413
+ style={cancelStyle}
414
+ className={slots.cancelClip()}
415
+ pointerEvents={cancelOut ? 'auto' : 'none'}
416
+ accessibilityElementsHidden={!cancelOut}
417
+ importantForAccessibility={cancelOut ? 'auto' : 'no-hide-descendants'}
418
+ >
419
+ {/*
420
+ * Absolute, and pinned to the end edge: it keeps its natural width
421
+ * inside a container whose width is animating, so the clip reveals
422
+ * it from the edge instead of squeezing the word as it arrives. It
423
+ * is also what makes the measurement possible at all — a child laid
424
+ * out against a container of width 0 would otherwise report 0.
425
+ */}
426
+ <AnimatedPressable
427
+ onLayout={handleCancelLayout}
428
+ onPress={handleCancel}
429
+ hitSlop={8}
430
+ accessibilityRole="button"
431
+ className={slots.cancelButton()}
432
+ >
433
+ <Text className={slots.cancelLabel()}>{cancelLabel}</Text>
434
+ </AnimatedPressable>
435
+ </Animated.View>
436
+ </View>
437
+ );
438
+ }
439
+ );
440
+
441
+ SearchBar.displayName = 'SearchBar';
@@ -0,0 +1,35 @@
1
+ export interface SearchBarDebounceTimer {
2
+ current: ReturnType<typeof setTimeout> | null;
3
+ }
4
+
5
+ export function cancelSearchBarDebounce(timer: SearchBarDebounceTimer): void {
6
+ if (timer.current === null) return;
7
+ clearTimeout(timer.current);
8
+ timer.current = null;
9
+ }
10
+
11
+ export function scheduleSearchBarDebounce(
12
+ timer: SearchBarDebounceTimer,
13
+ callback: ((value: string) => void) | undefined,
14
+ value: string,
15
+ delay: number
16
+ ): void {
17
+ cancelSearchBarDebounce(timer);
18
+ if (!Number.isFinite(delay) || delay <= 0) {
19
+ callback?.(value);
20
+ return;
21
+ }
22
+ timer.current = setTimeout(() => {
23
+ timer.current = null;
24
+ callback?.(value);
25
+ }, delay);
26
+ }
27
+
28
+ export function flushSearchBarDebounce(
29
+ timer: SearchBarDebounceTimer,
30
+ callback: ((value: string) => void) | undefined,
31
+ value: string
32
+ ): void {
33
+ cancelSearchBarDebounce(timer);
34
+ callback?.(value);
35
+ }
@@ -70,6 +70,7 @@ import { useBackHandler } from '../../hooks/use-back-handler';
70
70
  import { cn } from '../../utils/cn';
71
71
  import { BottomSheet } from '../bottom-sheet';
72
72
  import { InputGroup } from '../input-group';
73
+ import { nativeSelectSupportsOptions } from './native-select-contract';
73
74
 
74
75
  const selectVariants = tv({
75
76
  slots: {
@@ -339,7 +340,6 @@ function SelectRoot({
339
340
  const triggerRef = useRef<View>(null);
340
341
  const chevron = useSharedValue(0);
341
342
  const { height: screenHeight } = useWindowDimensions();
342
- const nativeUI = native ? getNativeUI() : null;
343
343
 
344
344
  const options = useMemo(() => {
345
345
  const collected: SelectItemProps[] = [];
@@ -347,6 +347,15 @@ function SelectRoot({
347
347
  return collected;
348
348
  }, [children]);
349
349
 
350
+ /*
351
+ * @expo/ui's portable Picker only has a control-wide `enabled` flag. Its
352
+ * items accept label and value, but no disabled state, so handing it a
353
+ * disabled Select.Item would make that option selectable again. Preserve
354
+ * the public item contract by using the styled Select for that list.
355
+ */
356
+ const nativeUI =
357
+ native && nativeSelectSupportsOptions(options) ? getNativeUI() : null;
358
+
350
359
  const selectedLabel = useMemo(
351
360
  () => options.find((option) => option.value === value)?.label,
352
361
  [options, value]
@@ -0,0 +1,12 @@
1
+ interface NativeSelectOption {
2
+ disabled?: boolean;
3
+ }
4
+
5
+ /**
6
+ * The portable @expo/ui picker can disable the whole control, but not one
7
+ * option. Keep the styled Select when an item needs that distinction rather
8
+ * than silently turning a disabled choice back into a selectable one.
9
+ */
10
+ export function nativeSelectSupportsOptions(options: NativeSelectOption[]): boolean {
11
+ return options.every((option) => !option.disabled);
12
+ }