@btndprojects/loam-ui-native 0.1.1 → 0.2.1

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 (262) hide show
  1. package/dist/accordion/index.js +5 -3
  2. package/dist/adaptive/index.js +6 -2
  3. package/dist/alert/index.js +5 -2
  4. package/dist/analyzer/index.js +9 -6
  5. package/dist/announcement/index.js +8 -5
  6. package/dist/audio-player/index.js +5 -3
  7. package/dist/avatar/index.js +5 -2
  8. package/dist/avatar-group/index.js +7 -3
  9. package/dist/badge/index.js +7 -4
  10. package/dist/banner/index.js +5 -2
  11. package/dist/board/index.js +11 -6
  12. package/dist/button/index.js +6 -3
  13. package/dist/camera-permission/index.js +15 -12
  14. package/dist/capture-result/index.js +19 -16
  15. package/dist/card/index.js +6 -3
  16. package/dist/card-form/index.js +12 -8
  17. package/dist/carousel/index.js +4 -2
  18. package/dist/chart/index.js +8 -5
  19. package/dist/charts/index.js +8 -6
  20. package/dist/chat/index.js +16 -0
  21. package/dist/checkbox/index.js +6 -4
  22. package/dist/checklist/index.js +11 -8
  23. package/dist/checkout/index.js +16 -11
  24. package/dist/chip/index.js +7 -4
  25. package/dist/{chunk-XNUVB5OL.js → chunk-2OLKPG4E.js} +92 -28
  26. package/dist/{chunk-RUZHTGKB.js → chunk-2PKYGLQ6.js} +71 -40
  27. package/dist/{chunk-UC2WGUP3.js → chunk-2SWKL3AJ.js} +91 -252
  28. package/dist/{chunk-AI4DWZCD.js → chunk-2SY63FCH.js} +63 -16
  29. package/dist/{chunk-F5XNPWS7.js → chunk-2TXERVVA.js} +92 -35
  30. package/dist/chunk-36EC2T6P.js +581 -0
  31. package/dist/chunk-3DUWIYV5.js +437 -0
  32. package/dist/{chunk-JNY4ZHAR.js → chunk-3QMEOFW5.js} +275 -71
  33. package/dist/chunk-3WEZCGHX.js +265 -0
  34. package/dist/chunk-46MMSDGL.js +194 -0
  35. package/dist/{chunk-7O2FMUPT.js → chunk-4IZXHVFJ.js} +297 -92
  36. package/dist/{chunk-LAECSR4H.js → chunk-5LAAAXFP.js} +14 -8
  37. package/dist/{chunk-2A5NIQAK.js → chunk-5RENVKBQ.js} +134 -46
  38. package/dist/{chunk-2ZZ7TDJ4.js → chunk-5UTRIXQ6.js} +37 -31
  39. package/dist/{chunk-LOE5YNN3.js → chunk-5VFZRGVD.js} +37 -10
  40. package/dist/{chunk-52X2VYTD.js → chunk-67IZZ2MJ.js} +115 -60
  41. package/dist/{chunk-UYG5L34B.js → chunk-6YAQBXB3.js} +10 -4
  42. package/dist/{chunk-BTTFCMKG.js → chunk-74FLKD24.js} +94 -23
  43. package/dist/{chunk-ITTYJZWX.js → chunk-7BUIKDD6.js} +73 -18
  44. package/dist/{chunk-423B7SPN.js → chunk-7OVVSVEZ.js} +273 -109
  45. package/dist/chunk-AEB7KCHQ.js +287 -0
  46. package/dist/chunk-APCWFOZW.js +28 -0
  47. package/dist/chunk-AWKQIE5I.js +139 -0
  48. package/dist/{chunk-VCDBXW5H.js → chunk-B7N4ASYE.js} +86 -42
  49. package/dist/{chunk-IVNRBPGC.js → chunk-C4GTDLRC.js} +104 -20
  50. package/dist/{chunk-4ZAHCY44.js → chunk-C6APNDVA.js} +12 -5
  51. package/dist/{chunk-4SZYY6AN.js → chunk-CMTVNF3D.js} +18 -13
  52. package/dist/{chunk-HZ6CNMAF.js → chunk-CPTOADB2.js} +151 -96
  53. package/dist/chunk-D5N5QWI7.js +1325 -0
  54. package/dist/{chunk-IIQTAY3J.js → chunk-DN7MNQIP.js} +113 -29
  55. package/dist/{chunk-47FWHGUN.js → chunk-DVGBZ3TX.js} +84 -23
  56. package/dist/chunk-DW3L54DQ.js +305 -0
  57. package/dist/{chunk-3ARE4TES.js → chunk-E2E3I5W2.js} +69 -18
  58. package/dist/{chunk-DHWADYZX.js → chunk-EDCBJV7J.js} +41 -28
  59. package/dist/{chunk-WSW6ZFAQ.js → chunk-ENP3BMZU.js} +4 -4
  60. package/dist/{chunk-OOPPQH27.js → chunk-EVOIGUBI.js} +43 -19
  61. package/dist/{chunk-VEAYTRPX.js → chunk-EY4DD67D.js} +165 -97
  62. package/dist/{chunk-223Q2T2Q.js → chunk-F3H33DQX.js} +132 -61
  63. package/dist/{chunk-CYRBHQQZ.js → chunk-F5B3UINI.js} +68 -35
  64. package/dist/{chunk-6X6PQ443.js → chunk-FAJI7BSC.js} +20 -9
  65. package/dist/{chunk-MOUJOP55.js → chunk-FUMHAAF3.js} +96 -29
  66. package/dist/{chunk-6HT2LP62.js → chunk-FV6HMP7W.js} +192 -73
  67. package/dist/{chunk-S7F6XLTK.js → chunk-FYWNRX4F.js} +76 -47
  68. package/dist/{chunk-YG3ZJHGU.js → chunk-FZGAAOKS.js} +30 -8
  69. package/dist/{chunk-YDGVJETP.js → chunk-G3AFC24V.js} +1 -1
  70. package/dist/{chunk-EJBDRVIH.js → chunk-G6UESEVN.js} +15 -8
  71. package/dist/chunk-GDKVKNR2.js +339 -0
  72. package/dist/chunk-GEIGMWNF.js +10 -0
  73. package/dist/{chunk-JUAMLEHX.js → chunk-H7DWSL4O.js} +24 -12
  74. package/dist/chunk-HJFFOOJ4.js +1 -0
  75. package/dist/{chunk-RW6ZL2FQ.js → chunk-HJLPZA4X.js} +106 -21
  76. package/dist/chunk-HYEOQWT7.js +647 -0
  77. package/dist/chunk-I2KLQ2HA.js +19 -0
  78. package/dist/{chunk-CJ5EOYZE.js → chunk-I4FHVKEC.js} +11 -5
  79. package/dist/{chunk-JNH2TYIN.js → chunk-IDM7JJMO.js} +19 -5
  80. package/dist/{chunk-O4AEHWII.js → chunk-IPUNJMAQ.js} +141 -49
  81. package/dist/{chunk-J7Q7VCVC.js → chunk-IT6FUXXG.js} +13 -6
  82. package/dist/{chunk-QJJ6MADV.js → chunk-IVKMZ7BB.js} +80 -28
  83. package/dist/{chunk-Z7H7VSFZ.js → chunk-IXKWIFWZ.js} +17 -6
  84. package/dist/chunk-J64KGCGX.js +55 -0
  85. package/dist/chunk-JBMLWFSJ.js +270 -0
  86. package/dist/{chunk-4BXMXBJO.js → chunk-JJLPBRVZ.js} +22 -11
  87. package/dist/chunk-JLC6PM5S.js +406 -0
  88. package/dist/chunk-JUYWSSKW.js +335 -0
  89. package/dist/{chunk-7CFF4HXS.js → chunk-JV5YOTOB.js} +49 -11
  90. package/dist/{chunk-LZLHELAG.js → chunk-KFJGFLMW.js} +29 -8
  91. package/dist/{chunk-O7AE6DAF.js → chunk-KQZ3GLHR.js} +18 -8
  92. package/dist/{chunk-N32RGOA5.js → chunk-LKFC4CS7.js} +80 -24
  93. package/dist/{chunk-YC6WORFL.js → chunk-LMPHCVJJ.js} +82 -50
  94. package/dist/{chunk-3WPW4KUN.js → chunk-LQP4KEXZ.js} +45 -13
  95. package/dist/{chunk-TKQYWZ5V.js → chunk-LUMVGO2K.js} +47 -31
  96. package/dist/{chunk-AWJOCGFZ.js → chunk-M656FYNH.js} +25 -13
  97. package/dist/{chunk-35OFI3JG.js → chunk-MKSNFG7H.js} +73 -20
  98. package/dist/{chunk-4GBOGOTV.js → chunk-NBEF4EPJ.js} +16 -6
  99. package/dist/chunk-NNCOFLNE.js +24 -0
  100. package/dist/{chunk-4YQOR7JM.js → chunk-NTQ2F3AU.js} +73 -20
  101. package/dist/{chunk-QCDXZBC5.js → chunk-O5V4TG3V.js} +126 -44
  102. package/dist/{chunk-U7TFM2JA.js → chunk-OJQUBQWA.js} +3 -3
  103. package/dist/{chunk-VQHPQEUH.js → chunk-OWAMM2DK.js} +13 -6
  104. package/dist/chunk-PID3NJB7.js +405 -0
  105. package/dist/{chunk-HET4LLMX.js → chunk-PMMTGBBS.js} +27 -17
  106. package/dist/{chunk-RIBDXGUG.js → chunk-PNF3NKB5.js} +49 -16
  107. package/dist/{chunk-JXJB2X6M.js → chunk-Q6NKEPRE.js} +81 -32
  108. package/dist/chunk-QDZGFXQD.js +176 -0
  109. package/dist/{chunk-SYNOJOQO.js → chunk-QEYFDRHR.js} +60 -26
  110. package/dist/{chunk-VBOXAEPI.js → chunk-QHAITQIF.js} +97 -22
  111. package/dist/{chunk-VXDZFJGP.js → chunk-QHTVYA65.js} +230 -76
  112. package/dist/{chunk-TUTI5HOZ.js → chunk-QIKT7LIB.js} +14 -8
  113. package/dist/{chunk-3ENTMPQY.js → chunk-QP3KMOVF.js} +82 -42
  114. package/dist/{chunk-GNNJ4GCC.js → chunk-QPWDDJJ4.js} +62 -148
  115. package/dist/{chunk-QWCMU5B2.js → chunk-QRUMA22E.js} +205 -80
  116. package/dist/{chunk-ZVFE5QGU.js → chunk-QYPE5JX3.js} +50 -10
  117. package/dist/{chunk-PUPM2YGF.js → chunk-ROCUG24E.js} +76 -36
  118. package/dist/{chunk-NQYBUO3K.js → chunk-RU3ZPNL6.js} +333 -142
  119. package/dist/{chunk-TOXAGRDQ.js → chunk-RXSCIO6W.js} +14 -5
  120. package/dist/{chunk-FMYDDECK.js → chunk-SATMR6SG.js} +631 -203
  121. package/dist/{chunk-4YIP3CWS.js → chunk-SHHY5OMK.js} +7 -5
  122. package/dist/chunk-SKWO3SZF.js +2284 -0
  123. package/dist/{chunk-YF4NSSMT.js → chunk-SWS5JQIF.js} +41 -13
  124. package/dist/{chunk-CUMUNPXY.js → chunk-T5LTPKX6.js} +15 -7
  125. package/dist/{chunk-H6NIOQHK.js → chunk-TDMG26JI.js} +26 -6
  126. package/dist/{chunk-UWFLYMAR.js → chunk-TLCIR6RG.js} +267 -126
  127. package/dist/{chunk-SBMNWWBV.js → chunk-TN2ZNJUI.js} +47 -9
  128. package/dist/{chunk-5YNTNN5A.js → chunk-TQVAIKFD.js} +106 -41
  129. package/dist/chunk-TRGFFU6A.js +1456 -0
  130. package/dist/{chunk-2GK6PHGE.js → chunk-TY4MTKQJ.js} +100 -22
  131. package/dist/{chunk-AZRX6SAC.js → chunk-U23FYWME.js} +15 -7
  132. package/dist/{chunk-PZXZJBMP.js → chunk-U5NLHYV6.js} +26 -12
  133. package/dist/chunk-UQEN55EY.js +1 -0
  134. package/dist/chunk-UQGBTPXH.js +218 -0
  135. package/dist/chunk-UXGIW7OK.js +116 -0
  136. package/dist/{chunk-PJXUJYS6.js → chunk-UYGDJWMN.js} +55 -30
  137. package/dist/{chunk-QDYDBCFP.js → chunk-V2OEB75Z.js} +50 -51
  138. package/dist/{chunk-LSS54GLJ.js → chunk-VAOAKWUK.js} +34 -13
  139. package/dist/{chunk-DM5QUKKO.js → chunk-VDPK5CY4.js} +18 -6
  140. package/dist/chunk-W23G6JLF.js +37 -0
  141. package/dist/{chunk-CDRZHT42.js → chunk-W63AJTM7.js} +276 -12
  142. package/dist/{chunk-XXXEFXY5.js → chunk-W74TXYFB.js} +13 -5
  143. package/dist/{chunk-LRJNDEIA.js → chunk-WRDSPUE7.js} +144 -29
  144. package/dist/{chunk-C2NTXVWM.js → chunk-XBZGDDMR.js} +11 -4
  145. package/dist/{chunk-BV3B2HYZ.js → chunk-XIDTBCBA.js} +189 -131
  146. package/dist/{chunk-YTFKPRUK.js → chunk-XN4KG3P4.js} +348 -153
  147. package/dist/chunk-XQXMLMH3.js +211 -0
  148. package/dist/{chunk-HPPW424V.js → chunk-XTORZWRF.js} +19 -8
  149. package/dist/{chunk-P23EQNRQ.js → chunk-YAAHPATV.js} +181 -30
  150. package/dist/{chunk-KMI5ML5I.js → chunk-YXOAPXMU.js} +57 -18
  151. package/dist/{chunk-RJQOOHT2.js → chunk-ZECZOOSB.js} +59 -28
  152. package/dist/{chunk-QKXCPTFS.js → chunk-ZLTO5VQI.js} +121 -31
  153. package/dist/{chunk-BEDBFJND.js → chunk-ZVATDZOM.js} +16 -18
  154. package/dist/coachmark/index.js +6 -3
  155. package/dist/code-block/index.js +5 -3
  156. package/dist/color-field/index.js +6 -4
  157. package/dist/commerce/index.js +19 -16
  158. package/dist/connectivity/index.js +5 -2
  159. package/dist/context-menu/index.js +5 -3
  160. package/dist/copy-button/index.js +4 -2
  161. package/dist/data-grid/index.js +21 -16
  162. package/dist/date-range-picker/index.js +11 -8
  163. package/dist/description-list/index.js +3 -1
  164. package/dist/dialog/index.js +4 -2
  165. package/dist/empty-state/index.js +4 -2
  166. package/dist/error-summary/index.js +4 -2
  167. package/dist/fab/index.js +5 -2
  168. package/dist/file-manager/index.js +28 -0
  169. package/dist/file-upload/index.js +10 -7
  170. package/dist/filter-bar/index.js +9 -6
  171. package/dist/flow-run/index.js +15 -12
  172. package/dist/form/index.js +5 -3
  173. package/dist/gallery/index.js +5 -2
  174. package/dist/hold-to-confirm/index.js +4 -2
  175. package/dist/index.d.ts +9557 -2985
  176. package/dist/index.js +355 -122
  177. package/dist/infinite-list/index.js +4 -2
  178. package/dist/inline-edit/index.js +6 -4
  179. package/dist/input/index.js +5 -3
  180. package/dist/interactions/index.js +2 -1
  181. package/dist/invoice/index.js +13 -9
  182. package/dist/kbd/index.js +5 -1
  183. package/dist/keyboard/index.js +4 -2
  184. package/dist/list-row/index.js +6 -4
  185. package/dist/log-viewer/index.js +4 -2
  186. package/dist/map-view/index.js +5 -3
  187. package/dist/markdown/index.js +9 -6
  188. package/dist/metric/index.js +7 -4
  189. package/dist/money/index.js +9 -3
  190. package/dist/nav-drawer/index.js +4 -2
  191. package/dist/nav-header/index.js +9 -0
  192. package/dist/navigation/index.js +5 -2
  193. package/dist/notification-center/index.js +12 -10
  194. package/dist/number-roll/index.js +5 -2
  195. package/dist/one-handed/index.js +5 -3
  196. package/dist/os-surfaces/index.js +6 -3
  197. package/dist/otp/index.js +7 -4
  198. package/dist/page-header/index.js +1 -0
  199. package/dist/pagination/index.js +6 -0
  200. package/dist/payment-method/index.js +14 -10
  201. package/dist/payment-status/index.js +17 -14
  202. package/dist/permission/index.js +5 -2
  203. package/dist/picker/index.js +11 -6
  204. package/dist/progress/index.js +6 -3
  205. package/dist/pull-to-refresh/index.js +4 -2
  206. package/dist/radio-group/index.js +6 -4
  207. package/dist/rating/index.js +5 -2
  208. package/dist/receivables/index.js +21 -16
  209. package/dist/refund/index.js +18 -14
  210. package/dist/save-status/index.js +5 -3
  211. package/dist/scroll-area/index.js +3 -1
  212. package/dist/search-field/index.js +8 -0
  213. package/dist/segmented-control/index.js +4 -3
  214. package/dist/sheet/index.js +5 -3
  215. package/dist/skeleton/index.js +3 -1
  216. package/dist/slider/index.js +4 -2
  217. package/dist/sortable/index.js +7 -0
  218. package/dist/spinner/index.js +3 -1
  219. package/dist/split-button/index.js +9 -6
  220. package/dist/spotlight/index.js +3 -1
  221. package/dist/stage-timeline/index.js +5 -3
  222. package/dist/status-dot/index.js +4 -2
  223. package/dist/stepper/index.js +7 -4
  224. package/dist/streaming-text/index.js +3 -2
  225. package/dist/survey/index.js +18 -15
  226. package/dist/switch/index.js +5 -3
  227. package/dist/system-state/index.js +7 -4
  228. package/dist/tab-bar/index.js +5 -2
  229. package/dist/tabs/index.js +4 -2
  230. package/dist/tags-input/index.js +10 -0
  231. package/dist/textarea/index.js +6 -4
  232. package/dist/thinking/index.js +3 -1
  233. package/dist/time-field/index.js +12 -0
  234. package/dist/time-range-control/index.js +22 -0
  235. package/dist/timeline/index.js +4 -2
  236. package/dist/toast/index.js +4 -2
  237. package/dist/toolbar/index.js +4 -2
  238. package/dist/tour/index.js +12 -9
  239. package/dist/truncation-notice/index.js +5 -2
  240. package/dist/update-notice/index.js +12 -9
  241. package/dist/video-player/index.js +5 -2
  242. package/dist/viewfinder/index.js +6 -4
  243. package/dist/widget/index.js +7 -4
  244. package/dist/wizard/index.js +7 -4
  245. package/package.json +13 -12
  246. package/dist/chunk-6N33LFQA.js +0 -35
  247. package/dist/chunk-GDKZTW34.js +0 -395
  248. package/dist/chunk-GL7B2DOW.js +0 -134
  249. package/dist/chunk-NKB7BZYK.js +0 -167
  250. package/dist/chunk-OETCUXQP.js +0 -134
  251. package/dist/chunk-OPXFZ4Y5.js +0 -204
  252. package/dist/chunk-Q72LJOVE.js +0 -169
  253. package/dist/chunk-RNGLIHFA.js +0 -191
  254. package/dist/chunk-RQ3HASCC.js +0 -6
  255. package/dist/chunk-RSOJT5YW.js +0 -124
  256. package/dist/chunk-TKVFVF5D.js +0 -95
  257. package/dist/chunk-XOSCY2CW.js +0 -242
  258. package/dist/header/index.js +0 -5
  259. package/dist/search-bar/index.js +0 -5
  260. /package/dist/{chunk-3ECPOSKO.js → chunk-3UG6JUCV.js} +0 -0
  261. /package/dist/{chunk-JRLTGLN4.js → chunk-5WWRTEPS.js} +0 -0
  262. /package/dist/{chunk-RTIKSY6A.js → chunk-5Z5ZYMOJ.js} +0 -0
@@ -1,10 +1,19 @@
1
- import { useColorTransition } from './chunk-ITTYJZWX.js';
1
+ import { useColorTransition } from './chunk-7BUIKDD6.js';
2
+ import { announce } from './chunk-W23G6JLF.js';
2
3
  import { haptics } from './chunk-NY2U3PBI.js';
4
+ import { useLabels } from './chunk-APCWFOZW.js';
5
+ import { Icon } from './chunk-UXGIW7OK.js';
6
+ import { springConfig } from './chunk-J64KGCGX.js';
3
7
  import * as React from 'react';
4
- import { Animated, Easing, Pressable, View, TextInput, Text } from 'react-native';
8
+ import { Animated, Easing, View, Pressable, TextInput, Platform, Text } from 'react-native';
5
9
  import { createStyleSheet, useStyles, useLoamTheme } from '@btndprojects/loam-theme-native';
6
10
  import { jsxs, jsx } from 'react/jsx-runtime';
7
11
 
12
+ var DEFAULT_LABELS = {
13
+ field: "One-time code",
14
+ incorrect: "Incorrect code",
15
+ errorAnnouncement: (field, message) => `${field}: ${message}`
16
+ };
8
17
  var MASK = "\u2022";
9
18
  var COMPLETE_POP = 1.08;
10
19
  var COMPLETE_STAGGER = 55;
@@ -17,7 +26,7 @@ function OTPBox({
17
26
  caret,
18
27
  pop
19
28
  }) {
20
- const theme = useLoamTheme();
29
+ const theme = useLoamTheme("otp");
21
30
  const { styles } = useStyles(sheet, { disabled: false });
22
31
  const easing = React.useMemo(() => Easing.bezier(...theme.easing.base), [theme.easing.base]);
23
32
  const borderColor = useColorTransition(borderTarget, theme.duration.base, easing);
@@ -39,18 +48,23 @@ function OTP({
39
48
  onComplete,
40
49
  disabled = false,
41
50
  error = false,
51
+ errorMessage: errorMessageProp,
42
52
  secure = false,
43
53
  keyboardType = "number-pad",
44
- accessibilityLabel = "One-time code",
54
+ accessibilityLabel: accessibilityLabelProp,
55
+ labels: labelsProp,
45
56
  style
46
57
  }) {
58
+ const labels = useLabels("otp", DEFAULT_LABELS, labelsProp);
59
+ const errorMessage = errorMessageProp ?? labels.incorrect;
60
+ const accessibilityLabel = accessibilityLabelProp ?? labels.field;
47
61
  const isControlled = value !== void 0;
48
62
  const [internal, setInternal] = React.useState(() => defaultValue.slice(0, length));
49
63
  const current = (isControlled ? value : internal) ?? "";
50
64
  const [focused, setFocused] = React.useState(false);
51
65
  const inputRef = React.useRef(null);
52
66
  const { styles } = useStyles(sheet, { disabled });
53
- const theme = useLoamTheme();
67
+ const theme = useLoamTheme("otp");
54
68
  const caret = React.useRef(new Animated.Value(1)).current;
55
69
  React.useEffect(() => {
56
70
  if (!focused || disabled || theme.reduceMotion) {
@@ -114,64 +128,105 @@ function OTP({
114
128
  anim.start();
115
129
  return () => anim.stop();
116
130
  }, [isComplete, disabled, pops, theme.duration.base, theme.easing.base]);
117
- return /* @__PURE__ */ jsxs(
118
- Pressable,
119
- {
120
- onPress: focus,
121
- disabled,
122
- accessibilityRole: "none",
123
- style: [styles.row, style],
124
- children: [
125
- /* @__PURE__ */ jsx(
126
- View,
127
- {
128
- style: styles.boxes,
129
- importantForAccessibility: "no-hide-descendants",
130
- accessibilityElementsHidden: true,
131
- children: Array.from({ length }).map((_, i) => {
132
- const char = current[i];
133
- const isActive = focused && !disabled && !error && i === activeIndex && i < length;
134
- const borderTarget = error ? theme.color.danger : isComplete ? theme.color.good : char || isActive ? theme.color.accent : theme.color.borderStrong;
135
- return /* @__PURE__ */ jsx(
136
- OTPBox,
137
- {
138
- char,
139
- secure,
140
- active: isActive,
141
- borderTarget,
142
- fillTarget: isActive ? theme.color.surface : theme.color.surface2,
143
- caret,
144
- pop: pops[i]
145
- },
146
- i
147
- );
148
- })
149
- }
150
- ),
151
- /* @__PURE__ */ jsx(
152
- TextInput,
153
- {
154
- ref: inputRef,
155
- value: current,
156
- onChangeText: handleChange,
157
- onFocus: () => setFocused(true),
158
- onBlur: () => setFocused(false),
159
- editable: !disabled,
160
- keyboardType,
161
- textContentType: "oneTimeCode",
162
- autoComplete: "one-time-code",
163
- maxLength: length,
164
- caretHidden: true,
165
- accessibilityLabel,
166
- accessibilityState: { disabled },
167
- style: styles.hiddenInput
168
- }
169
- )
170
- ]
131
+ const kick = React.useRef(new Animated.Value(0)).current;
132
+ const wasError = React.useRef(error);
133
+ React.useEffect(() => {
134
+ if (error && !wasError.current) {
135
+ haptics.notify("error");
136
+ announce(labels.errorAnnouncement(accessibilityLabel, errorMessage), "assertive");
137
+ if (!theme.reduceMotion) {
138
+ kick.setValue(theme.space[4]);
139
+ Animated.spring(kick, {
140
+ ...springConfig(theme, "fs", { thrown: true }),
141
+ toValue: 0,
142
+ useNativeDriver: true
143
+ }).start();
144
+ }
171
145
  }
172
- );
146
+ wasError.current = error;
147
+ }, [error, kick, theme, accessibilityLabel, errorMessage, labels]);
148
+ const errorId = React.useId();
149
+ const showError = error && !!errorMessage;
150
+ return /* @__PURE__ */ jsxs(View, { style: [styles.root, style], children: [
151
+ /* @__PURE__ */ jsxs(Pressable, { onPress: focus, disabled, accessibilityRole: "none", style: styles.row, children: [
152
+ /* @__PURE__ */ jsx(
153
+ Animated.View,
154
+ {
155
+ style: [styles.boxes, { transform: [{ translateX: kick }] }],
156
+ importantForAccessibility: "no-hide-descendants",
157
+ accessibilityElementsHidden: true,
158
+ children: Array.from({ length }).map((_, i) => {
159
+ const char = current[i];
160
+ const isActive = focused && !disabled && !error && i === activeIndex && i < length;
161
+ const borderTarget = error ? theme.color.danger : isComplete ? theme.color.good : char || isActive ? theme.color.accent : theme.color.borderStrong;
162
+ return /* @__PURE__ */ jsx(
163
+ OTPBox,
164
+ {
165
+ char,
166
+ secure,
167
+ active: isActive,
168
+ borderTarget,
169
+ fillTarget: isActive ? theme.color.surface : theme.color.surface2,
170
+ caret,
171
+ pop: pops[i]
172
+ },
173
+ i
174
+ );
175
+ })
176
+ }
177
+ ),
178
+ /* @__PURE__ */ jsx(
179
+ TextInput,
180
+ {
181
+ ref: inputRef,
182
+ value: current,
183
+ onChangeText: handleChange,
184
+ onFocus: () => setFocused(true),
185
+ onBlur: () => setFocused(false),
186
+ editable: !disabled,
187
+ keyboardType,
188
+ textContentType: "oneTimeCode",
189
+ autoComplete: "one-time-code",
190
+ maxLength: length,
191
+ caretHidden: true,
192
+ accessibilityLabel,
193
+ accessibilityState: { disabled },
194
+ secureTextEntry: secure,
195
+ accessibilityHint: showError && Platform.OS !== "web" ? errorMessage : void 0,
196
+ ...{
197
+ "aria-invalid": error || void 0,
198
+ "aria-describedby": showError ? errorId : void 0
199
+ },
200
+ style: styles.hiddenInput
201
+ }
202
+ )
203
+ ] }),
204
+ showError ? (
205
+ // Read through the field it describes; not a second swipe stop on native.
206
+ /* @__PURE__ */ jsxs(
207
+ View,
208
+ {
209
+ style: styles.errorRow,
210
+ importantForAccessibility: "no-hide-descendants",
211
+ accessibilityElementsHidden: true,
212
+ children: [
213
+ /* @__PURE__ */ jsx(Icon, { name: "alert-triangle", size: theme.icon.sm, color: theme.color.danger }),
214
+ /* @__PURE__ */ jsx(Text, { nativeID: errorId, style: styles.errorText, children: errorMessage })
215
+ ]
216
+ }
217
+ )
218
+ ) : null
219
+ ] });
173
220
  }
174
221
  var sheet = createStyleSheet((theme) => ({
222
+ root: { alignSelf: "flex-start", gap: theme.space.s2 },
223
+ errorRow: { flexDirection: "row", alignItems: "center", gap: theme.space.s1 },
224
+ errorText: {
225
+ fontFamily: theme.font.sans,
226
+ fontSize: theme.type.footnote.fontSize,
227
+ lineHeight: theme.type.footnote.lineHeight,
228
+ color: theme.color.danger
229
+ },
175
230
  row: {
176
231
  position: "relative",
177
232
  alignSelf: "flex-start",
@@ -1,9 +1,13 @@
1
- import { useThinkingPulse } from './chunk-XXXEFXY5.js';
1
+ import { useThinkingPulse } from './chunk-W74TXYFB.js';
2
+ import { useLabels } from './chunk-APCWFOZW.js';
2
3
  import 'react';
3
4
  import { View, Animated, Text } from 'react-native';
4
5
  import { createStyleSheet, useStyles, useLoamTheme } from '@btndprojects/loam-theme-native';
5
6
  import { jsxs, jsx } from 'react/jsx-runtime';
6
7
 
8
+ var DEFAULT_LABELS = {
9
+ status: (state) => `Status: ${state}`
10
+ };
7
11
  function StatusDot({
8
12
  state,
9
13
  size = "md",
@@ -12,13 +16,15 @@ function StatusDot({
12
16
  motion = "full",
13
17
  accessibilityLabel,
14
18
  style,
15
- testID
19
+ testID,
20
+ labels: labelsProp
16
21
  }) {
22
+ const labels = useLabels("statusDot", DEFAULT_LABELS, labelsProp);
17
23
  const { styles } = useStyles(sheet, { state, size });
18
- const theme = useLoamTheme();
24
+ const theme = useLoamTheme("status-dot");
19
25
  const shouldPulse = state === "working" && (pulse ?? true) && motion === "full" && !theme.reduceMotion;
20
26
  const pulseStyle = useThinkingPulse(shouldPulse);
21
- const name = accessibilityLabel || label || `Status: ${state}`;
27
+ const name = accessibilityLabel || label || labels.status(state);
22
28
  return /* @__PURE__ */ jsxs(
23
29
  View,
24
30
  {
@@ -1,13 +1,22 @@
1
- import { useColorTransition } from './chunk-ITTYJZWX.js';
1
+ import { useColorTransition, useFieldValidation } from './chunk-7BUIKDD6.js';
2
2
  import { NO_UA_FOCUS_RING } from './chunk-3NMD74ZU.js';
3
+ import { useLabels } from './chunk-APCWFOZW.js';
4
+ import { Icon } from './chunk-UXGIW7OK.js';
3
5
  import * as React from 'react';
4
- import { Easing, Animated, TextInput, View, Text } from 'react-native';
6
+ import { Easing, Animated, TextInput, ActivityIndicator, View, Text } from 'react-native';
5
7
  import { createStyleSheet, useStyles, useLoamTheme } from '@btndprojects/loam-theme-native';
6
- import { jsx, jsxs } from 'react/jsx-runtime';
8
+ import { jsxs, jsx } from 'react/jsx-runtime';
7
9
 
8
10
  var ROW_HEIGHT = 22;
11
+ var DEFAULT_LABELS = {
12
+ validating: "Validating",
13
+ valid: "Valid",
14
+ invalid: "Invalid"
15
+ };
9
16
  function Textarea({
10
17
  error = false,
18
+ success = false,
19
+ loading = false,
11
20
  rows = 3,
12
21
  autoGrow = false,
13
22
  minRows = 3,
@@ -18,24 +27,32 @@ function Textarea({
18
27
  onContentSizeChange,
19
28
  style,
20
29
  containerStyle,
30
+ labels: labelsProp,
21
31
  ...rest
22
32
  }) {
33
+ const labels = useLabels("textarea", DEFAULT_LABELS, labelsProp);
23
34
  const [focused, setFocused] = React.useState(false);
24
35
  const disabled = editable === false;
25
36
  const { styles } = useStyles(sheet, { disabled });
26
- const theme = useLoamTheme();
37
+ const theme = useLoamTheme("textarea");
27
38
  const padV = 2 * theme.space.s3;
28
39
  const minHeight = (autoGrow ? minRows : rows) * ROW_HEIGHT + padV;
29
40
  const maxHeight = maxRows * ROW_HEIGHT + padV;
30
41
  const [grownHeight, setGrownHeight] = React.useState(null);
31
42
  const handleContentSizeChange = (e) => {
32
43
  if (autoGrow) {
33
- const next = Math.min(Math.max(e.nativeEvent.contentSize.height + padV, minHeight), maxHeight);
44
+ const next = Math.min(
45
+ Math.max(e.nativeEvent.contentSize.height + padV, minHeight),
46
+ maxHeight
47
+ );
34
48
  setGrownHeight(next);
35
49
  }
36
50
  onContentSizeChange?.(e);
37
51
  };
38
- const tone = disabled ? { border: theme.color.border, fill: theme.color.surface3 } : error ? { border: theme.color.danger, fill: theme.color.dangerSoft } : focused ? { border: theme.focus.ringColor, fill: theme.color.surface } : { border: theme.color.borderStrong, fill: theme.color.surface2 };
52
+ const isLoading = loading && !disabled;
53
+ const invalid = !isLoading && !disabled && error;
54
+ const valid = !isLoading && !disabled && !error && success;
55
+ const tone = disabled ? { border: theme.color.border, fill: theme.color.surface3 } : isLoading ? { border: theme.focus.ringColor, fill: theme.color.surface } : invalid ? { border: theme.color.danger, fill: theme.color.dangerSoft } : valid ? { border: theme.color.good, fill: theme.color.goodSoft } : focused ? { border: theme.focus.ringColor, fill: theme.color.surface } : { border: theme.color.borderStrong, fill: theme.color.surface2 };
39
56
  const easing = React.useMemo(() => Easing.bezier(...theme.easing.base), [theme.easing.base]);
40
57
  const borderColor = useColorTransition(tone.border, theme.duration.base, easing);
41
58
  const backgroundColor = useColorTransition(tone.fill, theme.duration.base, easing);
@@ -47,7 +64,7 @@ function Textarea({
47
64
  setFocused(false);
48
65
  onBlur?.(e);
49
66
  };
50
- return /* @__PURE__ */ jsx(
67
+ return /* @__PURE__ */ jsxs(
51
68
  Animated.View,
52
69
  {
53
70
  testID: "textarea-container",
@@ -60,31 +77,72 @@ function Textarea({
60
77
  autoGrow && { maxHeight, height: grownHeight ?? minHeight }
61
78
  ],
62
79
  accessibilityState: { disabled },
63
- children: /* @__PURE__ */ jsx(
64
- TextInput,
65
- {
66
- ...rest,
67
- multiline: true,
68
- textAlignVertical: "top",
69
- editable,
70
- placeholderTextColor: theme.color.text3,
71
- onFocus: handleFocus,
72
- onBlur: handleBlur,
73
- onContentSizeChange: handleContentSizeChange,
74
- style: [styles.input, NO_UA_FOCUS_RING, style]
75
- }
76
- )
80
+ children: [
81
+ /* @__PURE__ */ jsx(
82
+ TextInput,
83
+ {
84
+ ...rest,
85
+ multiline: true,
86
+ textAlignVertical: "top",
87
+ editable,
88
+ placeholderTextColor: theme.color.text3,
89
+ "aria-invalid": invalid || void 0,
90
+ "aria-busy": isLoading || void 0,
91
+ onFocus: handleFocus,
92
+ onBlur: handleBlur,
93
+ onContentSizeChange: handleContentSizeChange,
94
+ style: [styles.input, NO_UA_FOCUS_RING, style]
95
+ }
96
+ ),
97
+ isLoading ? /* @__PURE__ */ jsx(
98
+ ActivityIndicator,
99
+ {
100
+ testID: "textarea-spinner",
101
+ size: "small",
102
+ color: theme.color.accent,
103
+ accessibilityLabel: labels.validating
104
+ }
105
+ ) : valid || invalid ? /* @__PURE__ */ jsx(
106
+ View,
107
+ {
108
+ testID: "textarea-status-icon",
109
+ accessibilityRole: "image",
110
+ accessibilityLabel: valid ? labels.valid : labels.invalid,
111
+ children: /* @__PURE__ */ jsx(
112
+ Icon,
113
+ {
114
+ name: valid ? "check-circle" : "alert-triangle",
115
+ size: theme.icon.sm,
116
+ color: valid ? theme.color.goodOnSoft : theme.color.dangerOnSoft
117
+ }
118
+ )
119
+ }
120
+ ) : null
121
+ ]
77
122
  }
78
123
  );
79
124
  }
80
125
  function TextareaField({
81
126
  label,
82
127
  hint,
83
- error,
128
+ error: errorProp,
129
+ success: successProp,
130
+ validate,
84
131
  containerStyle,
132
+ onBlur,
133
+ onChangeText,
85
134
  ...textareaProps
86
135
  }) {
87
136
  const { styles } = useStyles(fieldSheet);
137
+ const { error, success, ...handlers } = useFieldValidation({
138
+ validate,
139
+ error: errorProp,
140
+ success: successProp,
141
+ value: textareaProps.value,
142
+ onBlur,
143
+ onChangeText
144
+ });
145
+ const theme = useLoamTheme("textarea");
88
146
  const labelId = React.useId();
89
147
  return (
90
148
  // Sizing lands here, not on the inner `Textarea`: a label and helper line that
@@ -95,11 +153,19 @@ function TextareaField({
95
153
  Textarea,
96
154
  {
97
155
  ...textareaProps,
156
+ ...handlers,
157
+ success,
98
158
  "aria-labelledby": label ? labelId : textareaProps["aria-labelledby"],
99
159
  error: Boolean(error)
100
160
  }
101
161
  ),
102
- error ? /* @__PURE__ */ jsx(Text, { style: styles.error, children: error }) : hint ? /* @__PURE__ */ jsx(Text, { style: styles.hint, children: hint }) : null
162
+ error ? (
163
+ // Colour, icon and text together; the alert role announces it when it appears.
164
+ /* @__PURE__ */ jsxs(View, { accessibilityRole: "alert", style: styles.errorRow, children: [
165
+ /* @__PURE__ */ jsx(Icon, { name: "alert-triangle", size: theme.icon.sm, color: theme.color.dangerOnSoft }),
166
+ /* @__PURE__ */ jsx(Text, { style: styles.error, children: error })
167
+ ] })
168
+ ) : hint ? /* @__PURE__ */ jsx(Text, { style: styles.hint, children: hint }) : null
103
169
  ] })
104
170
  );
105
171
  }
@@ -110,6 +176,9 @@ var sheet = createStyleSheet((theme) => ({
110
176
  // The reference draws a `1.5px` field edge — the static `w2` weight.
111
177
  borderWidth: theme.borderWidth.thick,
112
178
  borderRadius: theme.radius.md,
179
+ flexDirection: "row",
180
+ alignItems: "flex-start",
181
+ gap: theme.space.s2,
113
182
  paddingHorizontal: theme.space.s3,
114
183
  paddingVertical: theme.space.s3,
115
184
  variants: {
@@ -156,7 +225,9 @@ var fieldSheet = createStyleSheet((theme) => ({
156
225
  fontSize: theme.fontSize[12],
157
226
  color: theme.color.text3
158
227
  },
228
+ errorRow: { flexDirection: "row", alignItems: "center", gap: theme.space.s1 },
159
229
  error: {
230
+ flexShrink: 1,
160
231
  fontFamily: theme.font.sans,
161
232
  fontSize: theme.fontSize[12],
162
233
  color: theme.color.danger
@@ -1,19 +1,53 @@
1
1
  import { NO_UA_FOCUS_RING } from './chunk-3NMD74ZU.js';
2
- import { useResolvedMotion } from './chunk-6N33LFQA.js';
3
- import { Icon } from './chunk-TKVFVF5D.js';
4
- import * as React from 'react';
2
+ import { useLabels } from './chunk-APCWFOZW.js';
3
+ import { Icon } from './chunk-UXGIW7OK.js';
4
+ import { useResolvedMotion } from './chunk-J64KGCGX.js';
5
+ import * as React2 from 'react';
5
6
  import { Animated, Easing, View, TextInput, ActivityIndicator, Text } from 'react-native';
6
7
  import { createStyleSheet, useStyles, useLoamTheme } from '@btndprojects/loam-theme-native';
7
8
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
8
9
 
10
+ function useFieldValidation({
11
+ validate,
12
+ error,
13
+ success,
14
+ value,
15
+ onBlur,
16
+ onChangeText
17
+ }) {
18
+ const [touched, setTouched] = React2.useState(false);
19
+ const [verdict, setVerdict] = React2.useState(null);
20
+ const lastText = React2.useRef(value ?? "");
21
+ if (value !== void 0) lastText.current = value;
22
+ const run = (text) => {
23
+ const message = validate?.(text);
24
+ setVerdict({ message: message ? String(message) : void 0, filled: text !== "" });
25
+ };
26
+ if (!validate) return { error, success, onBlur, onChangeText };
27
+ const shownError = error ?? verdict?.message;
28
+ return {
29
+ error: shownError,
30
+ success: success ?? (verdict !== null && !shownError && verdict.filled ? true : void 0),
31
+ onBlur: (event) => {
32
+ setTouched(true);
33
+ run(lastText.current);
34
+ onBlur?.(event);
35
+ },
36
+ onChangeText: (text) => {
37
+ lastText.current = text;
38
+ if (touched) run(text);
39
+ onChangeText?.(text);
40
+ }
41
+ };
42
+ }
9
43
  function useColorTransition(target, duration, easing, animate = true) {
10
- const progress = React.useRef(new Animated.Value(1)).current;
11
- const [range, setRange] = React.useState([target, target]);
12
- React.useEffect(() => {
44
+ const progress = React2.useRef(new Animated.Value(1)).current;
45
+ const [range, setRange] = React2.useState([target, target]);
46
+ React2.useEffect(() => {
13
47
  setRange((prev) => prev[1] === target ? prev : [prev[1], target]);
14
48
  }, [target]);
15
49
  const [from, to] = range;
16
- React.useEffect(() => {
50
+ React2.useEffect(() => {
17
51
  if (from === to) return;
18
52
  if (!animate) {
19
53
  progress.setValue(1);
@@ -24,6 +58,11 @@ function useColorTransition(target, duration, easing, animate = true) {
24
58
  }, [from, to, progress, duration, easing, animate]);
25
59
  return progress.interpolate({ inputRange: [0, 1], outputRange: [from, to] });
26
60
  }
61
+ var DEFAULT_LABELS = {
62
+ validating: "Validating",
63
+ valid: "Valid",
64
+ invalid: "Invalid"
65
+ };
27
66
  function Input({
28
67
  size = "md",
29
68
  error = false,
@@ -40,21 +79,23 @@ function Input({
40
79
  onBlur,
41
80
  style,
42
81
  containerStyle,
82
+ labels: labelsProp,
43
83
  ...rest
44
84
  }) {
45
- const [focused, setFocused] = React.useState(false);
46
- const animateColour = useResolvedMotion(motion) === "full";
85
+ const labels = useLabels("input", DEFAULT_LABELS, labelsProp);
86
+ const [focused, setFocused] = React2.useState(false);
87
+ const animateColour = useResolvedMotion(motion, "input") === "full";
47
88
  const disabled = editable === false;
48
89
  const isLoading = loading && !disabled;
49
90
  const valid = !isLoading && (validationState === "valid" || success);
50
91
  const invalid = !isLoading && (validationState === "invalid" || error);
51
92
  const { styles } = useStyles(sheet, { size, disabled });
52
- const theme = useLoamTheme();
93
+ const theme = useLoamTheme("input");
53
94
  const iconSize = theme.ctl.icon[size];
54
95
  const iconColor = theme.color.text3;
55
96
  const slopY = Math.max(0, (theme.hit[size] - theme.ctl[size]) / 2);
56
97
  const tone = disabled ? { border: theme.color.border, fill: theme.color.surface3 } : isLoading ? { border: theme.focus.ringColor, fill: theme.color.surface } : invalid ? { border: theme.color.danger, fill: theme.color.dangerSoft } : valid ? { border: theme.color.good, fill: theme.color.goodSoft } : focused ? { border: theme.focus.ringColor, fill: theme.color.surface } : { border: theme.color.borderStrong, fill: theme.color.surface2 };
57
- const easing = React.useMemo(() => Easing.bezier(...theme.easing.base), [theme.easing.base]);
98
+ const easing = React2.useMemo(() => Easing.bezier(...theme.easing.base), [theme.easing.base]);
58
99
  const borderColor = useColorTransition(tone.border, theme.duration.base, easing, animateColour);
59
100
  const backgroundColor = useColorTransition(tone.fill, theme.duration.base, easing, animateColour);
60
101
  const statusIconName = valid ? "check-circle" : invalid ? "alert-triangle" : void 0;
@@ -62,8 +103,8 @@ function Input({
62
103
  const showStatus = showStatusIcon && statusIconName !== void 0;
63
104
  const messageColor = invalid ? theme.color.danger : valid ? theme.color.goodOnSoft : theme.color.text2;
64
105
  const showMessageRow = validationState !== "default" || message !== void 0;
65
- const statusOpacity = React.useRef(new Animated.Value(0)).current;
66
- React.useEffect(() => {
106
+ const statusOpacity = React2.useRef(new Animated.Value(0)).current;
107
+ React2.useEffect(() => {
67
108
  const toValue = showStatus ? 1 : 0;
68
109
  if (!animateColour) {
69
110
  statusOpacity.setValue(toValue);
@@ -115,7 +156,7 @@ function Input({
115
156
  testID: "input-spinner",
116
157
  size: "small",
117
158
  color: theme.color.accent,
118
- accessibilityLabel: "Validating"
159
+ accessibilityLabel: labels.validating
119
160
  }
120
161
  )
121
162
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -125,7 +166,7 @@ function Input({
125
166
  {
126
167
  testID: "input-status-icon",
127
168
  accessibilityRole: "image",
128
- accessibilityLabel: valid ? "Valid" : "Invalid",
169
+ accessibilityLabel: valid ? labels.valid : labels.invalid,
129
170
  style: { opacity: statusOpacity },
130
171
  children: /* @__PURE__ */ jsx(Icon, { name: statusIconName, size: iconSize, color: statusColor })
131
172
  }
@@ -140,12 +181,24 @@ function Input({
140
181
  function TextField({
141
182
  label,
142
183
  hint,
143
- error,
184
+ error: errorProp,
185
+ success: successProp,
186
+ validate,
144
187
  containerStyle,
188
+ onBlur,
189
+ onChangeText,
145
190
  ...inputProps
146
191
  }) {
147
192
  const { styles } = useStyles(fieldSheet);
148
- const labelId = React.useId();
193
+ const { error, success, ...handlers } = useFieldValidation({
194
+ validate,
195
+ error: errorProp,
196
+ success: successProp,
197
+ value: inputProps.value,
198
+ onBlur,
199
+ onChangeText
200
+ });
201
+ const labelId = React2.useId();
149
202
  return (
150
203
  // The caller's sizing goes here rather than on the inner `Input`: a label and
151
204
  // a helper line that did not follow the field's width would read as a
@@ -156,6 +209,8 @@ function TextField({
156
209
  Input,
157
210
  {
158
211
  ...inputProps,
212
+ ...handlers,
213
+ success,
159
214
  "aria-labelledby": label ? labelId : inputProps["aria-labelledby"],
160
215
  error: Boolean(error)
161
216
  }
@@ -268,4 +323,4 @@ var fieldSheet = createStyleSheet((theme) => ({
268
323
  }
269
324
  }));
270
325
 
271
- export { Input, TextField, useColorTransition };
326
+ export { Input, TextField, useColorTransition, useFieldValidation };