@expokit/cli 0.0.0-stage → 0.2.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 (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -2
  3. package/bin/expo-kit.js +1204 -0
  4. package/bin/lib.mjs +343 -0
  5. package/bin/mcp.mjs +338 -0
  6. package/package.json +44 -4
  7. package/registry/_docs/accordion.json +99 -0
  8. package/registry/_docs/action-sheet.json +110 -0
  9. package/registry/_docs/alert.json +127 -0
  10. package/registry/_docs/avatar.json +203 -0
  11. package/registry/_docs/badge.json +117 -0
  12. package/registry/_docs/bottom-nav.json +119 -0
  13. package/registry/_docs/bottom-sheet.json +158 -0
  14. package/registry/_docs/bullet.json +24 -0
  15. package/registry/_docs/button-3d.json +122 -0
  16. package/registry/_docs/button.json +115 -0
  17. package/registry/_docs/calendar.json +182 -0
  18. package/registry/_docs/card-stack.json +108 -0
  19. package/registry/_docs/card.json +111 -0
  20. package/registry/_docs/carousel.json +102 -0
  21. package/registry/_docs/checkbox.json +93 -0
  22. package/registry/_docs/checkout.json +102 -0
  23. package/registry/_docs/chip-group.json +116 -0
  24. package/registry/_docs/color-swatch.json +66 -0
  25. package/registry/_docs/confetti.json +82 -0
  26. package/registry/_docs/coupon.json +176 -0
  27. package/registry/_docs/cube-pager.json +84 -0
  28. package/registry/_docs/dashboard.json +120 -0
  29. package/registry/_docs/date-picker.json +249 -0
  30. package/registry/_docs/dialog.json +162 -0
  31. package/registry/_docs/drawer.json +171 -0
  32. package/registry/_docs/fab.json +120 -0
  33. package/registry/_docs/flip-card.json +111 -0
  34. package/registry/_docs/floating-input.json +191 -0
  35. package/registry/_docs/gift-card.json +108 -0
  36. package/registry/_docs/glass-button.json +118 -0
  37. package/registry/_docs/glass-nav-bar.json +78 -0
  38. package/registry/_docs/glass-search-bar.json +90 -0
  39. package/registry/_docs/glass-segmented.json +66 -0
  40. package/registry/_docs/glass-sheet.json +108 -0
  41. package/registry/_docs/glass-tab-bar.json +76 -0
  42. package/registry/_docs/glass.json +78 -0
  43. package/registry/_docs/inbox.json +72 -0
  44. package/registry/_docs/isometric-bar.json +72 -0
  45. package/registry/_docs/kit.json +11 -0
  46. package/registry/_docs/kpi-card.json +11 -0
  47. package/registry/_docs/like-button.json +72 -0
  48. package/registry/_docs/liquidglass-tab.json +27 -0
  49. package/registry/_docs/list-item.json +200 -0
  50. package/registry/_docs/loyalty-card.json +108 -0
  51. package/registry/_docs/morph-button.json +108 -0
  52. package/registry/_docs/number-ticker.json +96 -0
  53. package/registry/_docs/offers.json +78 -0
  54. package/registry/_docs/onboarding.json +66 -0
  55. package/registry/_docs/otp-input.json +134 -0
  56. package/registry/_docs/pagination-dots.json +88 -0
  57. package/registry/_docs/parallax-layers.json +84 -0
  58. package/registry/_docs/parallax-scroll.json +186 -0
  59. package/registry/_docs/password-input.json +92 -0
  60. package/registry/_docs/payment-card.json +153 -0
  61. package/registry/_docs/pixel-bar.json +66 -0
  62. package/registry/_docs/polar-area.json +78 -0
  63. package/registry/_docs/profile.json +84 -0
  64. package/registry/_docs/progress-steps.json +92 -0
  65. package/registry/_docs/progress.json +132 -0
  66. package/registry/_docs/radio-group.json +92 -0
  67. package/registry/_docs/rating.json +90 -0
  68. package/registry/_docs/ring-carousel.json +102 -0
  69. package/registry/_docs/scratch-card.json +90 -0
  70. package/registry/_docs/search-bar.json +140 -0
  71. package/registry/_docs/search-results.json +120 -0
  72. package/registry/_docs/segmented-control.json +103 -0
  73. package/registry/_docs/select.json +252 -0
  74. package/registry/_docs/settings.json +72 -0
  75. package/registry/_docs/shimmer-button.json +78 -0
  76. package/registry/_docs/sign-in.json +198 -0
  77. package/registry/_docs/skeleton.json +142 -0
  78. package/registry/_docs/slide-to-confirm.json +84 -0
  79. package/registry/_docs/slider.json +157 -0
  80. package/registry/_docs/spin-wheel.json +114 -0
  81. package/registry/_docs/spinner.json +91 -0
  82. package/registry/_docs/stepper.json +168 -0
  83. package/registry/_docs/swipe-deck.json +102 -0
  84. package/registry/_docs/swipeable-row.json +72 -0
  85. package/registry/_docs/switch.json +104 -0
  86. package/registry/_docs/tabs.json +105 -0
  87. package/registry/_docs/tag-input.json +128 -0
  88. package/registry/_docs/textarea.json +175 -0
  89. package/registry/_docs/ticket-card.json +174 -0
  90. package/registry/_docs/ticket-shape.json +11 -0
  91. package/registry/_docs/tilt-card.json +141 -0
  92. package/registry/_docs/time-picker.json +253 -0
  93. package/registry/_docs/timeline-chart.json +30 -0
  94. package/registry/_docs/timeline.json +156 -0
  95. package/registry/_docs/toast.json +161 -0
  96. package/registry/_docs/treemap.json +30 -0
  97. package/registry/_docs/waterfall.json +36 -0
  98. package/registry/_docs/wheel-list.json +102 -0
  99. package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
  100. package/registry/accordion/files/src/lib/kit.tsx +219 -0
  101. package/registry/accordion.json +17 -0
  102. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
  103. package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
  104. package/registry/action-sheet.json +18 -0
  105. package/registry/alert/files/src/components/ui/alert.tsx +654 -0
  106. package/registry/alert/files/src/components/ui/button.tsx +274 -0
  107. package/registry/alert/files/src/lib/kit.tsx +219 -0
  108. package/registry/alert.json +20 -0
  109. package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
  110. package/registry/avatar/files/src/lib/kit.tsx +219 -0
  111. package/registry/avatar.json +18 -0
  112. package/registry/badge/files/src/components/ui/badge.tsx +316 -0
  113. package/registry/badge/files/src/lib/kit.tsx +219 -0
  114. package/registry/badge.json +17 -0
  115. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
  116. package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
  117. package/registry/bottom-nav.json +18 -0
  118. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
  119. package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
  120. package/registry/bottom-sheet.json +17 -0
  121. package/registry/bullet.json +14 -0
  122. package/registry/button/files/src/components/ui/button.tsx +274 -0
  123. package/registry/button/files/src/lib/kit.tsx +219 -0
  124. package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
  125. package/registry/button-3d/files/src/lib/kit.tsx +219 -0
  126. package/registry/button-3d.json +18 -0
  127. package/registry/button.json +18 -0
  128. package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
  129. package/registry/calendar/files/src/lib/kit.tsx +219 -0
  130. package/registry/calendar.json +17 -0
  131. package/registry/card/files/src/components/ui/card.tsx +300 -0
  132. package/registry/card/files/src/lib/kit.tsx +219 -0
  133. package/registry/card-stack.json +15 -0
  134. package/registry/card.json +17 -0
  135. package/registry/carousel.json +15 -0
  136. package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
  137. package/registry/checkbox/files/src/lib/kit.tsx +219 -0
  138. package/registry/checkbox.json +17 -0
  139. package/registry/checkout.json +26 -0
  140. package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
  141. package/registry/chip-group/files/src/lib/kit.tsx +219 -0
  142. package/registry/chip-group.json +17 -0
  143. package/registry/color-swatch.json +17 -0
  144. package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
  145. package/registry/confetti/files/src/lib/kit.tsx +219 -0
  146. package/registry/confetti.json +15 -0
  147. package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
  148. package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
  149. package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
  150. package/registry/coupon/files/src/lib/kit.tsx +219 -0
  151. package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
  152. package/registry/coupon.json +23 -0
  153. package/registry/cube-pager.json +15 -0
  154. package/registry/dashboard.json +32 -0
  155. package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
  156. package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
  157. package/registry/date-picker/files/src/lib/kit.tsx +219 -0
  158. package/registry/date-picker.json +19 -0
  159. package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
  160. package/registry/dialog/files/src/lib/kit.tsx +219 -0
  161. package/registry/dialog.json +18 -0
  162. package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
  163. package/registry/drawer/files/src/lib/kit.tsx +219 -0
  164. package/registry/drawer.json +18 -0
  165. package/registry/fab.json +18 -0
  166. package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
  167. package/registry/flip-card/files/src/lib/kit.tsx +219 -0
  168. package/registry/flip-card.json +15 -0
  169. package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
  170. package/registry/floating-input/files/src/lib/kit.tsx +219 -0
  171. package/registry/floating-input.json +17 -0
  172. package/registry/gift-card.json +21 -0
  173. package/registry/glass-button.json +22 -0
  174. package/registry/glass-nav-bar.json +21 -0
  175. package/registry/glass-search-bar.json +21 -0
  176. package/registry/glass-segmented.json +22 -0
  177. package/registry/glass-sheet.json +22 -0
  178. package/registry/glass-tab-bar.json +22 -0
  179. package/registry/glass.json +19 -0
  180. package/registry/inbox.json +30 -0
  181. package/registry/isometric-bar.json +14 -0
  182. package/registry/kit/files/src/lib/kit.tsx +219 -0
  183. package/registry/kit.json +11 -0
  184. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
  185. package/registry/kpi-card.json +14 -0
  186. package/registry/like-button.json +18 -0
  187. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
  188. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
  189. package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
  190. package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
  191. package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
  192. package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
  193. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
  194. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
  195. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
  196. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
  197. package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
  198. package/registry/liquidglass-tab.json +23 -0
  199. package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
  200. package/registry/list-item/files/src/lib/kit.tsx +219 -0
  201. package/registry/list-item.json +17 -0
  202. package/registry/loyalty-card.json +23 -0
  203. package/registry/morph-button.json +18 -0
  204. package/registry/number-ticker.json +15 -0
  205. package/registry/offers.json +24 -0
  206. package/registry/onboarding.json +17 -0
  207. package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
  208. package/registry/otp-input/files/src/lib/kit.tsx +219 -0
  209. package/registry/otp-input.json +17 -0
  210. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
  211. package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
  212. package/registry/pagination-dots.json +15 -0
  213. package/registry/parallax-layers.json +15 -0
  214. package/registry/parallax-scroll.json +17 -0
  215. package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
  216. package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
  217. package/registry/password-input/files/src/lib/kit.tsx +219 -0
  218. package/registry/password-input.json +19 -0
  219. package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
  220. package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
  221. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
  222. package/registry/payment-card/files/src/lib/kit.tsx +219 -0
  223. package/registry/payment-card.json +21 -0
  224. package/registry/pixel-bar.json +14 -0
  225. package/registry/polar-area.json +14 -0
  226. package/registry/profile.json +17 -0
  227. package/registry/progress/files/src/components/ui/progress.tsx +883 -0
  228. package/registry/progress/files/src/lib/kit.tsx +219 -0
  229. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
  230. package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
  231. package/registry/progress-steps.json +18 -0
  232. package/registry/progress.json +17 -0
  233. package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
  234. package/registry/radio-group/files/src/lib/kit.tsx +219 -0
  235. package/registry/radio-group.json +18 -0
  236. package/registry/rating.json +18 -0
  237. package/registry/ring-carousel.json +15 -0
  238. package/registry/scratch-card.json +18 -0
  239. package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
  240. package/registry/search-bar/files/src/lib/kit.tsx +219 -0
  241. package/registry/search-bar.json +17 -0
  242. package/registry/search-results.json +30 -0
  243. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
  244. package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
  245. package/registry/segmented-control.json +18 -0
  246. package/registry/select/files/src/components/ui/select.tsx +1398 -0
  247. package/registry/select/files/src/lib/kit.tsx +219 -0
  248. package/registry/select.json +17 -0
  249. package/registry/settings.json +26 -0
  250. package/registry/shimmer-button.json +17 -0
  251. package/registry/sign-in.json +26 -0
  252. package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
  253. package/registry/skeleton/files/src/lib/kit.tsx +219 -0
  254. package/registry/skeleton.json +17 -0
  255. package/registry/slide-to-confirm.json +18 -0
  256. package/registry/slider/files/src/components/ui/slider.tsx +716 -0
  257. package/registry/slider/files/src/lib/kit.tsx +219 -0
  258. package/registry/slider.json +17 -0
  259. package/registry/spin-wheel.json +18 -0
  260. package/registry/spinner/files/src/components/ui/spinner.tsx +335 -0
  261. package/registry/spinner/files/src/lib/kit.tsx +219 -0
  262. package/registry/spinner.json +17 -0
  263. package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
  264. package/registry/stepper/files/src/lib/kit.tsx +219 -0
  265. package/registry/stepper.json +17 -0
  266. package/registry/swipe-deck.json +15 -0
  267. package/registry/swipeable-row.json +15 -0
  268. package/registry/switch/files/src/components/ui/switch.tsx +344 -0
  269. package/registry/switch/files/src/lib/kit.tsx +219 -0
  270. package/registry/switch.json +18 -0
  271. package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
  272. package/registry/tabs/files/src/lib/kit.tsx +219 -0
  273. package/registry/tabs.json +17 -0
  274. package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
  275. package/registry/tag-input/files/src/lib/kit.tsx +219 -0
  276. package/registry/tag-input.json +17 -0
  277. package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
  278. package/registry/textarea/files/src/lib/kit.tsx +219 -0
  279. package/registry/textarea.json +17 -0
  280. package/registry/ticket-card.json +24 -0
  281. package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
  282. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
  283. package/registry/ticket-shape.json +17 -0
  284. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
  285. package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
  286. package/registry/tilt-card.json +17 -0
  287. package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
  288. package/registry/time-picker/files/src/lib/kit.tsx +219 -0
  289. package/registry/time-picker.json +17 -0
  290. package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
  291. package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
  292. package/registry/timeline/files/src/lib/kit.tsx +219 -0
  293. package/registry/timeline-chart.json +14 -0
  294. package/registry/timeline.json +20 -0
  295. package/registry/toast/files/src/components/ui/button.tsx +274 -0
  296. package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
  297. package/registry/toast/files/src/lib/kit.tsx +219 -0
  298. package/registry/toast.json +20 -0
  299. package/registry/treemap.json +14 -0
  300. package/registry/waterfall.json +14 -0
  301. package/registry/wheel-list.json +17 -0
  302. package/schema/components.schema.json +13 -0
@@ -0,0 +1,528 @@
1
+ import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
2
+ import {
3
+ Animated,
4
+ Easing,
5
+ Platform,
6
+ Pressable,
7
+ StyleSheet,
8
+ Text,
9
+ TextInput,
10
+ View,
11
+ type StyleProp,
12
+ type TextInputProps,
13
+ type TextStyle,
14
+ type ViewStyle,
15
+ } from 'react-native';
16
+ import Svg, { Circle, Path } from 'react-native-svg';
17
+
18
+ import { alpha, motion, useKit } from '@/lib/kit';
19
+
20
+ export type FloatingInputVariant = 'outlined' | 'filled' | 'underline';
21
+
22
+ export interface FloatingInputProps extends Omit<TextInputProps, 'style' | 'placeholder'> {
23
+ /** Sits inside the field like a placeholder, then floats onto the border when focused or filled. */
24
+ label: string;
25
+ /** `false` puts the label above the field as static text (no notch) and shows `placeholder` inside. */
26
+ floatingLabel?: boolean;
27
+ /** Hint inside the empty field — only used when `floatingLabel` is false. */
28
+ placeholder?: string;
29
+ variant?: FloatingInputVariant;
30
+ /** Error message. Turns the field red, shows it with an icon and shakes the field once when it appears. */
31
+ error?: string;
32
+ /** Valid state: success border and an animated check. A string is shown as the message under the field. */
33
+ success?: boolean | string;
34
+ /** Hint under the field (replaced by the error / success message while one is shown). */
35
+ helper?: string;
36
+ /** Colour of the focused border, halo and floating label (default: theme accent). */
37
+ accent?: string;
38
+ /** Background behind the field — the floating label cuts its notch with this colour. */
39
+ surface?: string;
40
+ left?: React.ReactNode;
41
+ right?: React.ReactNode;
42
+ /** Text before the value, shown once the label floats — '₹', '+91', 'https://'. */
43
+ prefix?: string;
44
+ /** Text after the value, shown once the label floats — 'INR', 'kg', '.com'. */
45
+ suffix?: string;
46
+ /** Show an animated clear button while there is text. */
47
+ clearable?: boolean;
48
+ /** Show `length/maxLength` under the field (needs `maxLength`). */
49
+ counter?: boolean;
50
+ /** 'sm' 44pt / 14px · 'md' 54pt / 16px · 'lg' 64pt / 18px — type, padding and icons scale too. */
51
+ size?: 'sm' | 'md' | 'lg';
52
+ /** Root wrapper (the one that shakes) — margins, width, flex. */
53
+ style?: StyleProp<ViewStyle>;
54
+ /**
55
+ * The bordered box: background, border width / colour, radius, height, padding. Applied after
56
+ * the computed style, so it wins. A `borderColor` replaces the resting border only — focus
57
+ * (accent), error and success colours still take over in those states. A numeric `height` /
58
+ * `borderRadius` also re-centres the label and resizes the focus halo.
59
+ */
60
+ fieldStyle?: StyleProp<ViewStyle>;
61
+ /** The typed text — fontSize, fontWeight, color, letterSpacing, fontFamily… */
62
+ inputStyle?: StyleProp<TextStyle>;
63
+ /**
64
+ * The label, floating or above the field. Its `color` is the resting colour; focus (accent),
65
+ * error (danger) and success colours still take over in those states. With `fontSize` the
66
+ * floating label rests at that size and floats at the same ratio as the default, and the notch
67
+ * follows the rendered label width.
68
+ */
69
+ labelStyle?: StyleProp<TextStyle>;
70
+ /** Helper / error / success text under the field. `color` applies to the helper; error and success keep their tone colour. */
71
+ messageStyle?: StyleProp<TextStyle>;
72
+ /** `prefix` text. */
73
+ prefixStyle?: StyleProp<TextStyle>;
74
+ /** `suffix` text. */
75
+ suffixStyle?: StyleProp<TextStyle>;
76
+ /** The `length/maxLength` counter. Turns danger at the limit regardless. */
77
+ counterStyle?: StyleProp<TextStyle>;
78
+ /** Built-in icons at rest — the clear button (default: theme mutedForeground). */
79
+ iconColor?: string;
80
+ }
81
+
82
+ /** Everything that scales with `size`: height, type, padding, icons and the floated label. */
83
+ export const FLOATING_INPUT_SIZES = {
84
+ sm: { height: 44, font: 14, floated: 11, padX: 12, icon: 16, radius: -2 },
85
+ md: { height: 54, font: 16, floated: 12.5, padX: 16, icon: 18, radius: 0 },
86
+ lg: { height: 64, font: 18, floated: 13.5, padX: 18, icon: 20, radius: 4 },
87
+ };
88
+ const HALO = 4;
89
+ /** Font size of the static label used when `floatingLabel` is false. */
90
+ const LABEL_ABOVE_FONT = { sm: 13, md: 14, lg: 14.5 };
91
+ /** react-native-web draws its own focus rectangle; our border + halo is the focus indicator. */
92
+ const NO_WEB_OUTLINE = Platform.OS === 'web' ? ({ outlineStyle: 'none', outlineWidth: 0 } as object) : null;
93
+
94
+ /**
95
+ * Web only: browser autofill paints its own (blue) background and fills the input without an
96
+ * onChangeText, so the label would sit on top of the text. This stylesheet keeps the field's own
97
+ * background and fires a 1ms animation we listen for to float the label.
98
+ */
99
+ const AUTOFILL_CSS = `@keyframes ekAutofillOn{from{}to{}}@keyframes ekAutofillOff{from{}to{}}
100
+ input:-webkit-autofill,input:-webkit-autofill:hover,input:-webkit-autofill:focus{animation:ekAutofillOn 1ms;-webkit-text-fill-color:currentColor!important;caret-color:currentColor;transition:background-color 99999s 0s,color 99999s 0s!important}
101
+ input:not(:-webkit-autofill){animation:ekAutofillOff 1ms}`;
102
+ function useWebAutofill(inputRef: React.RefObject<TextInput | null>, onChange: (filled: boolean) => void) {
103
+ useEffect(() => {
104
+ if (Platform.OS !== 'web' || typeof document === 'undefined') return;
105
+ if (!document.getElementById('expokit-autofill')) {
106
+ const tag = document.createElement('style');
107
+ tag.id = 'expokit-autofill';
108
+ tag.textContent = AUTOFILL_CSS;
109
+ document.head.appendChild(tag);
110
+ }
111
+ const node = inputRef.current as unknown as HTMLElement | null;
112
+ if (!node?.addEventListener) return;
113
+ const onAnim = (e: Event) => {
114
+ const name = (e as AnimationEvent).animationName;
115
+ if (name === 'ekAutofillOn') onChange(true);
116
+ else if (name === 'ekAutofillOff') onChange(false);
117
+ };
118
+ node.addEventListener('animationstart', onAnim);
119
+ return () => node.removeEventListener('animationstart', onAnim);
120
+ // eslint-disable-next-line react-hooks/exhaustive-deps
121
+ }, []);
122
+ }
123
+
124
+ /**
125
+ * Text field whose label starts as the placeholder and glides up onto the border on focus,
126
+ * cutting a notch in the outline (Material-style). Focus draws a soft halo, errors shake,
127
+ * success pops a check — all spring-driven.
128
+ */
129
+ export const FloatingInput = forwardRef<TextInput, FloatingInputProps>(function FloatingInput(
130
+ {
131
+ label,
132
+ floatingLabel = true,
133
+ placeholder,
134
+ variant = 'outlined',
135
+ error,
136
+ success,
137
+ helper,
138
+ accent,
139
+ surface,
140
+ left,
141
+ right,
142
+ prefix,
143
+ suffix,
144
+ clearable,
145
+ counter,
146
+ size = 'md',
147
+ style,
148
+ fieldStyle,
149
+ inputStyle,
150
+ labelStyle: labelStyleProp,
151
+ messageStyle,
152
+ prefixStyle,
153
+ suffixStyle,
154
+ counterStyle,
155
+ iconColor,
156
+ placeholderTextColor,
157
+ value,
158
+ defaultValue,
159
+ onChangeText,
160
+ onFocus,
161
+ onBlur,
162
+ maxLength,
163
+ editable = true,
164
+ ...props
165
+ },
166
+ ref
167
+ ) {
168
+ const { colors, radius, font } = useKit();
169
+ const ff = font.family ? { fontFamily: font.family } : null;
170
+ const S = FLOATING_INPUT_SIZES[size];
171
+ // The user's border colour only replaces the resting one; numeric height / radius drive the layout maths.
172
+ const { borderColor: userBorder, ...userField } = StyleSheet.flatten(fieldStyle) ?? {};
173
+ const userLabelColor = StyleSheet.flatten(labelStyleProp)?.color;
174
+ const HEIGHT = typeof userField.height === 'number' ? userField.height : S.height;
175
+ // Floated size stays relative to the resting size, so a bigger label floats bigger at the same ratio.
176
+ const FLOAT_SCALE = S.floated / S.font;
177
+ const corner = typeof userField.borderRadius === 'number' ? userField.borderRadius : radius.scale(12 + S.radius);
178
+ const adorn = S.padX + S.icon + 10; // room for a left / right adornment
179
+ const slot = S.icon + 10; // room for the clear button / check
180
+ const tint = accent ?? colors.accent;
181
+ const bg = surface ?? colors.background;
182
+ const inputRef = useRef<TextInput>(null);
183
+ useImperativeHandle(ref, () => inputRef.current as TextInput);
184
+
185
+ const [focused, setFocused] = useState(false);
186
+ const [inner, setInner] = useState(defaultValue ?? '');
187
+ const [autofilled, setAutofilled] = useState(false);
188
+ useWebAutofill(inputRef, setAutofilled);
189
+ const text = value ?? inner;
190
+ const floating = focused || text.length > 0 || autofilled;
191
+ const isFocused = focused && editable;
192
+ const ok = !error && !!success;
193
+ const successText = typeof success === 'string' && ok ? success : undefined;
194
+
195
+ const float = useRef(new Animated.Value(floating ? 1 : 0)).current; // native: label + affixes
196
+ const glow = useRef(new Animated.Value(0)).current; // native: halo + indicator bar
197
+ const focus = useRef(new Animated.Value(0)).current; // JS: border / label colour
198
+ const shake = useRef(new Animated.Value(0)).current; // native
199
+ const clear = useRef(new Animated.Value(clearable && text ? 1 : 0)).current; // native
200
+ const check = useRef(new Animated.Value(ok ? 1 : 0)).current; // native
201
+
202
+ useEffect(() => {
203
+ Animated.spring(float, { toValue: floating ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
204
+ }, [floating, float]);
205
+
206
+ useEffect(() => {
207
+ Animated.timing(focus, { toValue: isFocused ? 1 : 0, duration: 180, easing: Easing.out(Easing.quad), useNativeDriver: false }).start();
208
+ Animated.spring(glow, { toValue: isFocused ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
209
+ }, [isFocused, focus, glow]);
210
+
211
+ useEffect(() => {
212
+ Animated.spring(clear, { toValue: clearable && text && editable ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
213
+ }, [clearable, text, editable, clear]);
214
+
215
+ useEffect(() => {
216
+ Animated.spring(check, { toValue: ok ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
217
+ }, [ok, check]);
218
+
219
+ useEffect(() => {
220
+ if (!error) return;
221
+ shake.setValue(0);
222
+ Animated.sequence(
223
+ [10, -8, 6, -4, 0].map((x) => Animated.timing(shake, { toValue: x, duration: 55, useNativeDriver: true }))
224
+ ).start();
225
+ }, [error, shake]);
226
+
227
+ const setText = (next: string) => {
228
+ if (value === undefined) setInner(next);
229
+ onChangeText?.(next);
230
+ };
231
+
232
+ const active = error ? colors.danger : tint;
233
+ const rest = error ? colors.danger : ok ? colors.success : ((userBorder as string | undefined) ?? colors.input);
234
+ const borderColor = focus.interpolate({ inputRange: [0, 1], outputRange: [rest, active] });
235
+ const labelColor = focus.interpolate({
236
+ inputRange: [0, 1],
237
+ outputRange: [error ? colors.danger : ok ? colors.success : ((userLabelColor as string | undefined) ?? colors.mutedForeground), active],
238
+ });
239
+ const glowClamped = glow.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' });
240
+
241
+ const outlined = variant === 'outlined';
242
+ const filled = variant === 'filled';
243
+ const underline = variant === 'underline';
244
+ const leftPad = underline ? (left ? S.icon + 14 : 2) : left ? adorn : S.padX;
245
+ const edge = right ? adorn : underline ? 0 : S.padX - 4;
246
+ const rightPad = (right ? adorn : underline ? 2 : S.padX) + (clearable ? slot : 0) + (ok ? slot : 0);
247
+ // Filled / underline push the text down so the floated label has room above it.
248
+ const contentTop = !floatingLabel ? 0 : filled ? HEIGHT / 3.2 : underline ? HEIGHT / 4.5 : 0;
249
+ const restCenter = (HEIGHT + contentTop) / 2;
250
+ const floatCenter = outlined ? 0 : filled ? HEIGHT * 0.26 : HEIGHT * 0.2;
251
+ const floatY = floatCenter - restCenter;
252
+ const floatX = outlined ? S.padX - 4 - leftPad : 0;
253
+
254
+ const labelStyle = {
255
+ transform: [
256
+ { translateY: float.interpolate({ inputRange: [0, 1], outputRange: [0, floatY] }) },
257
+ { translateX: float.interpolate({ inputRange: [0, 1], outputRange: [0, floatX] }) },
258
+ { scale: float.interpolate({ inputRange: [0, 1], outputRange: [1, FLOAT_SCALE] }) },
259
+ ],
260
+ };
261
+ // With the label above the field there is nothing to make room for, so affixes always show.
262
+ const affixOpacity = floatingLabel ? float.interpolate({ inputRange: [0.6, 1], outputRange: [0, 1], extrapolate: 'clamp' }) : 1;
263
+ const topLabelColor = focus.interpolate({
264
+ inputRange: [0, 1],
265
+ outputRange: [error ? colors.danger : ((userLabelColor as string | undefined) ?? colors.foreground), active],
266
+ });
267
+
268
+ const boxStyle: ViewStyle = outlined
269
+ ? { borderWidth: 1.5, borderRadius: corner, backgroundColor: bg }
270
+ : filled
271
+ ? { borderRadius: corner, backgroundColor: colors.muted, overflow: 'hidden' }
272
+ : { backgroundColor: 'transparent' };
273
+
274
+ const message = error ?? successText ?? helper;
275
+ const tone: MessageTone = error ? 'error' : successText ? 'success' : 'helper';
276
+ const toneColor = tone === 'error' ? colors.danger : tone === 'success' ? colors.success : colors.mutedForeground;
277
+ const restIcon = iconColor ?? colors.mutedForeground;
278
+ const showCounter = counter && maxLength !== undefined;
279
+
280
+ return (
281
+ <Animated.View style={[{ transform: [{ translateX: shake }] }, style]}>
282
+ {floatingLabel ? null : (
283
+ <Animated.Text
284
+ numberOfLines={1}
285
+ onPress={editable ? () => inputRef.current?.focus() : undefined}
286
+ // The input already carries the label for screen readers.
287
+ accessible={false}
288
+ importantForAccessibility="no"
289
+ accessibilityElementsHidden
290
+ style={[
291
+ styles.topLabel,
292
+ ff,
293
+ { fontSize: LABEL_ABOVE_FONT[size] },
294
+ underline && styles.topLabelFlush,
295
+ labelStyleProp,
296
+ { color: topLabelColor },
297
+ !editable && styles.disabled,
298
+ ]}
299
+ >
300
+ {label}
301
+ </Animated.Text>
302
+ )}
303
+ <View style={!editable && styles.disabled}>
304
+ {underline ? null : (
305
+ <Animated.View
306
+ pointerEvents="none"
307
+ style={[
308
+ styles.halo,
309
+ {
310
+ borderRadius: corner + HALO,
311
+ backgroundColor: active,
312
+ opacity: glow.interpolate({ inputRange: [0, 1], outputRange: [0, 0.18], extrapolate: 'clamp' }),
313
+ transform: [{ scale: glow.interpolate({ inputRange: [0, 1], outputRange: [0.97, 1] }) }],
314
+ },
315
+ ]}
316
+ />
317
+ )}
318
+ <Pressable onPress={() => inputRef.current?.focus()} disabled={!editable} accessible={false}>
319
+ <Animated.View style={[styles.field, boxStyle, { height: HEIGHT }, userField, { borderColor }]}>
320
+ {left ? <View style={[styles.left, { left: underline ? 2 : S.padX - 2 }, underline && { top: contentTop }]}>{left}</View> : null}
321
+ <View style={[styles.row, { paddingLeft: leftPad, paddingRight: rightPad, paddingTop: contentTop }]}>
322
+ {prefix ? (
323
+ <Animated.Text style={[styles.affix, styles.prefix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, prefixStyle, { opacity: affixOpacity }]}>
324
+ {prefix}
325
+ </Animated.Text>
326
+ ) : null}
327
+ <TextInput
328
+ ref={inputRef}
329
+ accessibilityLabel={label}
330
+ {...props}
331
+ maxLength={maxLength}
332
+ editable={editable}
333
+ value={text}
334
+ onChangeText={setText}
335
+ onFocus={(e) => {
336
+ setFocused(true);
337
+ onFocus?.(e);
338
+ }}
339
+ onBlur={(e) => {
340
+ setFocused(false);
341
+ onBlur?.(e);
342
+ }}
343
+ placeholder={floatingLabel ? undefined : placeholder}
344
+ placeholderTextColor={placeholderTextColor ?? colors.mutedForeground}
345
+ selectionColor={active}
346
+ cursorColor={active}
347
+ style={[styles.input, ff, { color: colors.foreground, fontSize: S.font }, inputStyle, NO_WEB_OUTLINE]}
348
+ />
349
+ {suffix ? (
350
+ <Animated.Text style={[styles.affix, styles.suffix, ff, { fontSize: S.font }, { color: colors.mutedForeground }, suffixStyle, { opacity: affixOpacity }]}>
351
+ {suffix}
352
+ </Animated.Text>
353
+ ) : null}
354
+ </View>
355
+ {floatingLabel ? (
356
+ <Animated.View
357
+ pointerEvents="none"
358
+ style={[styles.labelWrap, { left: leftPad, right: right ? adorn : S.padX - 4, top: contentTop, height: HEIGHT - contentTop }, labelStyle]}
359
+ >
360
+ <Animated.Text
361
+ numberOfLines={1}
362
+ style={[
363
+ styles.label,
364
+ ff,
365
+ {
366
+ fontSize: S.font,
367
+ // The notch: label paints the surface colour over the border line, so it is
368
+ // always exactly as wide as the rendered label.
369
+ backgroundColor: outlined ? bg : 'transparent',
370
+ paddingHorizontal: outlined ? 5 : 0,
371
+ marginLeft: outlined ? -5 : 0,
372
+ },
373
+ labelStyleProp,
374
+ { color: labelColor },
375
+ ]}
376
+ >
377
+ {label}
378
+ </Animated.Text>
379
+ </Animated.View>
380
+ ) : null}
381
+ {clearable ? (
382
+ <Animated.View
383
+ style={[styles.slot, { right: edge, top: contentTop, opacity: clear, transform: [{ scale: clear }] }]}
384
+ pointerEvents={text && editable ? 'box-none' : 'none'}
385
+ >
386
+ <Pressable
387
+ hitSlop={(44 - S.icon) / 2}
388
+ accessibilityRole="button"
389
+ accessibilityLabel={`Clear ${label}`}
390
+ onPress={() => {
391
+ setText('');
392
+ inputRef.current?.focus();
393
+ }}
394
+ >
395
+ <Svg width={S.icon} height={S.icon} viewBox="0 0 24 24">
396
+ <Circle cx={12} cy={12} r={11} fill={alpha(restIcon, 0.7)} />
397
+ <Path d="M8.5 8.5l7 7M15.5 8.5l-7 7" stroke={bg} strokeWidth={2.4} strokeLinecap="round" />
398
+ </Svg>
399
+ </Pressable>
400
+ </Animated.View>
401
+ ) : null}
402
+ <Animated.View
403
+ pointerEvents="none"
404
+ style={[
405
+ styles.slot,
406
+ {
407
+ right: edge + (clearable ? slot : 0),
408
+ top: contentTop,
409
+ opacity: check.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
410
+ transform: [
411
+ { scale: check.interpolate({ inputRange: [0, 1], outputRange: [0.3, 1] }) },
412
+ { rotate: check.interpolate({ inputRange: [0, 1], outputRange: ['-30deg', '0deg'] }) },
413
+ ],
414
+ },
415
+ ]}
416
+ >
417
+ <Svg width={S.icon + 2} height={S.icon + 2} viewBox="0 0 24 24" fill="none">
418
+ <Circle cx={12} cy={12} r={11} fill={alpha(colors.success, 0.16)} />
419
+ <Path d="M7.5 12.5l3 3 6-6.5" stroke={colors.success} strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" />
420
+ </Svg>
421
+ </Animated.View>
422
+ {right ? <View style={[styles.right, { right: underline ? 0 : S.padX - 4, top: contentTop }]}>{right}</View> : null}
423
+ {outlined ? null : (
424
+ <>
425
+ <View pointerEvents="none" style={[styles.baseLine, { height: underline ? 1.5 : 1, backgroundColor: rest }]} />
426
+ {/* Grows out from the centre on focus. */}
427
+ <Animated.View
428
+ pointerEvents="none"
429
+ style={[styles.bar, { backgroundColor: active, transform: [{ scaleX: glowClamped }] }]}
430
+ />
431
+ </>
432
+ )}
433
+ </Animated.View>
434
+ </Pressable>
435
+ </View>
436
+ {message || showCounter ? (
437
+ <View style={[styles.footer, underline && styles.footerFlush]}>
438
+ <View style={styles.messageSlot}>
439
+ {message ? <FieldMessage key={tone + message} text={message} tone={tone} color={toneColor} style={messageStyle} /> : null}
440
+ </View>
441
+ {showCounter ? (
442
+ <Text
443
+ style={[
444
+ styles.count,
445
+ ff,
446
+ { color: colors.mutedForeground },
447
+ counterStyle,
448
+ text.length >= maxLength! && { color: colors.danger },
449
+ ]}
450
+ >
451
+ {text.length}/{maxLength}
452
+ </Text>
453
+ ) : null}
454
+ </View>
455
+ ) : null}
456
+ </Animated.View>
457
+ );
458
+ });
459
+
460
+ type MessageTone = 'error' | 'success' | 'helper';
461
+
462
+ /** Helper / error / success line. Error and success slide up and fade in. */
463
+ function FieldMessage({ text, tone, color, style }: { text: string; tone: MessageTone; color: string; style?: StyleProp<TextStyle> }) {
464
+ const { font } = useKit();
465
+ const enter = useRef(new Animated.Value(tone === 'helper' ? 1 : 0)).current; // native
466
+ useEffect(() => {
467
+ if (tone !== 'helper') Animated.spring(enter, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
468
+ }, [tone, enter]);
469
+
470
+ return (
471
+ <Animated.View
472
+ style={[
473
+ styles.message,
474
+ {
475
+ opacity: enter.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
476
+ transform: [{ translateY: enter.interpolate({ inputRange: [0, 1], outputRange: [4, 0] }) }],
477
+ },
478
+ ]}
479
+ >
480
+ {tone === 'helper' ? null : (
481
+ <Svg width={14} height={14} viewBox="0 0 24 24" fill="none" style={styles.messageIcon}>
482
+ <Circle cx={12} cy={12} r={9.5} stroke={color} strokeWidth={1.8} />
483
+ {tone === 'error' ? (
484
+ <>
485
+ <Path d="M12 7.5v5.25" stroke={color} strokeWidth={1.8} strokeLinecap="round" />
486
+ <Circle cx={12} cy={16.3} r={1.1} fill={color} />
487
+ </>
488
+ ) : (
489
+ <Path d="M8 12.3l2.7 2.7 5.3-5.6" stroke={color} strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" />
490
+ )}
491
+ </Svg>
492
+ )}
493
+ <Text
494
+ accessibilityLiveRegion="polite"
495
+ style={[styles.helper, font.family ? { fontFamily: font.family } : null, { color }, style, tone !== 'helper' && { color }]}
496
+ >
497
+ {text}
498
+ </Text>
499
+ </Animated.View>
500
+ );
501
+ }
502
+
503
+ const styles = StyleSheet.create({
504
+ disabled: { opacity: 0.5 },
505
+ halo: { position: 'absolute', top: -HALO, left: -HALO, right: -HALO, bottom: -HALO },
506
+ field: { justifyContent: 'center' },
507
+ row: { flexDirection: 'row', alignItems: 'center', height: '100%' },
508
+ input: { flex: 1, alignSelf: 'stretch', paddingVertical: 0, paddingHorizontal: 0 },
509
+ affix: { fontWeight: '400' },
510
+ prefix: { marginRight: 4 },
511
+ suffix: { marginLeft: 6 },
512
+ labelWrap: { position: 'absolute', justifyContent: 'center', alignItems: 'flex-start', transformOrigin: 'left center' },
513
+ label: { fontWeight: '400' },
514
+ topLabel: { fontWeight: '400', marginBottom: 6, marginHorizontal: 2 },
515
+ topLabelFlush: { marginHorizontal: 0 },
516
+ left: { position: 'absolute', left: 14, top: 0, bottom: 0, justifyContent: 'center' },
517
+ right: { position: 'absolute', right: 12, top: 0, bottom: 0, justifyContent: 'center' },
518
+ slot: { position: 'absolute', top: 0, bottom: 0, justifyContent: 'center' },
519
+ baseLine: { position: 'absolute', left: 0, right: 0, bottom: 0 },
520
+ bar: { position: 'absolute', left: 0, right: 0, bottom: 0, height: 2 },
521
+ footer: { flexDirection: 'row', alignItems: 'flex-start', gap: 12, marginTop: 6, marginHorizontal: 4 },
522
+ footerFlush: { marginHorizontal: 0 },
523
+ messageSlot: { flex: 1 },
524
+ message: { flexDirection: 'row', alignItems: 'flex-start', gap: 5 },
525
+ messageIcon: { marginTop: 1.5 },
526
+ helper: { flexShrink: 1, fontSize: 12.5, lineHeight: 17 },
527
+ count: { fontSize: 12.5, lineHeight: 17, fontWeight: '400', fontVariant: ['tabular-nums'] },
528
+ });