@expokit/cli 0.4.4 → 0.5.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 (239) hide show
  1. package/README.md +30 -3
  2. package/bin/expo-kit.js +749 -288
  3. package/bin/lib.mjs +310 -3
  4. package/package.json +1 -1
  5. package/registry/_docs/action-sheet.json +1 -1
  6. package/registry/_docs/alert.json +6 -0
  7. package/registry/_docs/bottom-sheet.json +6 -0
  8. package/registry/_docs/bullet.json +6 -0
  9. package/registry/_docs/dialog.json +1 -1
  10. package/registry/_docs/drawer.json +3 -3
  11. package/registry/_docs/glass-search-bar.json +1 -1
  12. package/registry/_docs/grain.json +6 -1
  13. package/registry/_docs/isometric-bar.json +6 -0
  14. package/registry/_docs/liquidglass-tab.json +1 -1
  15. package/registry/_docs/password-input.json +1 -1
  16. package/registry/_docs/pixel-bar.json +6 -0
  17. package/registry/_docs/polar-area.json +6 -0
  18. package/registry/_docs/qr-code-button.json +9 -3
  19. package/registry/_docs/scratch-card.json +7 -1
  20. package/registry/_docs/search-bar.json +1 -1
  21. package/registry/_docs/select.json +2 -2
  22. package/registry/_docs/slide-to-confirm.json +2 -2
  23. package/registry/_docs/spinner.json +1 -1
  24. package/registry/_docs/stateful-button.json +2 -2
  25. package/registry/_docs/swipe-deck.json +1 -1
  26. package/registry/_docs/time-picker.json +2 -2
  27. package/registry/_docs/timeline-chart.json +6 -0
  28. package/registry/_docs/toast.json +15 -3
  29. package/registry/_docs/treemap.json +6 -0
  30. package/registry/_docs/waterfall.json +6 -0
  31. package/registry/accordion/files/src/components/ui/accordion.tsx +6 -4
  32. package/registry/accordion/files/src/lib/kit.tsx +363 -11
  33. package/registry/accordion.json +1 -0
  34. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +43 -48
  35. package/registry/action-sheet/files/src/lib/kit.tsx +363 -11
  36. package/registry/action-sheet.json +1 -0
  37. package/registry/alert/files/src/components/ui/alert.tsx +19 -12
  38. package/registry/alert/files/src/lib/kit.tsx +363 -11
  39. package/registry/alert.json +3 -4
  40. package/registry/assistant-app.json +1 -0
  41. package/registry/avatar/files/src/components/ui/avatar.tsx +84 -66
  42. package/registry/avatar/files/src/lib/kit.tsx +363 -11
  43. package/registry/avatar.json +1 -0
  44. package/registry/badge/files/src/components/ui/badge.tsx +34 -34
  45. package/registry/badge/files/src/lib/kit.tsx +363 -11
  46. package/registry/badge.json +1 -0
  47. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +28 -27
  48. package/registry/bottom-nav/files/src/lib/kit.tsx +363 -11
  49. package/registry/bottom-nav.json +5 -0
  50. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +353 -289
  51. package/registry/bottom-sheet/files/src/lib/kit.tsx +363 -11
  52. package/registry/bottom-sheet.json +5 -1
  53. package/registry/bullet.json +1 -0
  54. package/registry/button/files/src/components/ui/button.tsx +18 -16
  55. package/registry/button/files/src/lib/kit.tsx +363 -11
  56. package/registry/button-3d/files/src/components/ui/button-3d.tsx +2 -2
  57. package/registry/button-3d/files/src/lib/kit.tsx +363 -11
  58. package/registry/button-3d.json +1 -0
  59. package/registry/button.json +1 -0
  60. package/registry/calendar/files/src/components/ui/calendar.tsx +74 -76
  61. package/registry/calendar/files/src/lib/kit.tsx +363 -11
  62. package/registry/calendar.json +1 -0
  63. package/registry/card/files/src/components/ui/card.tsx +3 -3
  64. package/registry/card/files/src/lib/kit.tsx +363 -11
  65. package/registry/card-stack.json +1 -0
  66. package/registry/card.json +1 -0
  67. package/registry/carousel.json +1 -0
  68. package/registry/chat.json +7 -0
  69. package/registry/checkbox/files/src/components/ui/checkbox.tsx +4 -4
  70. package/registry/checkbox/files/src/lib/kit.tsx +363 -11
  71. package/registry/checkbox.json +1 -0
  72. package/registry/checkout.json +1 -0
  73. package/registry/chip-group/files/src/components/ui/chip-group.tsx +22 -22
  74. package/registry/chip-group/files/src/lib/kit.tsx +363 -11
  75. package/registry/chip-group.json +1 -0
  76. package/registry/confetti/files/src/components/ui/confetti.tsx +15 -14
  77. package/registry/confetti.json +3 -6
  78. package/registry/coupon/files/src/components/ui/coupon.tsx +42 -42
  79. package/registry/coupon/files/src/components/ui/flip-card.tsx +6 -5
  80. package/registry/coupon/files/src/components/ui/tilt-card.tsx +76 -91
  81. package/registry/coupon/files/src/lib/kit.tsx +363 -11
  82. package/registry/coupon/files/src/lib/ticket-shape.tsx +54 -44
  83. package/registry/coupon.json +1 -0
  84. package/registry/cube-pager.json +1 -0
  85. package/registry/dashboard.json +1 -0
  86. package/registry/date-picker/files/src/components/ui/calendar.tsx +74 -76
  87. package/registry/date-picker/files/src/components/ui/date-picker.tsx +55 -56
  88. package/registry/date-picker/files/src/lib/kit.tsx +363 -11
  89. package/registry/date-picker.json +1 -0
  90. package/registry/dialog/files/src/components/ui/dialog.tsx +60 -56
  91. package/registry/dialog/files/src/lib/kit.tsx +363 -11
  92. package/registry/dialog.json +1 -0
  93. package/registry/donut-shop.json +1 -0
  94. package/registry/drawer/files/src/components/ui/drawer.tsx +73 -69
  95. package/registry/drawer/files/src/lib/kit.tsx +363 -11
  96. package/registry/drawer.json +1 -0
  97. package/registry/fab.json +1 -0
  98. package/registry/fintech-app.json +1 -0
  99. package/registry/flip-calendar/files/src/components/ui/flip-calendar.tsx +92 -86
  100. package/registry/flip-calendar/files/src/lib/kit.tsx +363 -11
  101. package/registry/flip-calendar.json +7 -0
  102. package/registry/flip-card/files/src/components/ui/flip-card.tsx +6 -5
  103. package/registry/flip-card/files/src/lib/kit.tsx +363 -11
  104. package/registry/flip-card.json +1 -0
  105. package/registry/floating-input/files/src/components/ui/floating-input.tsx +33 -33
  106. package/registry/floating-input/files/src/lib/kit.tsx +363 -11
  107. package/registry/floating-input.json +1 -0
  108. package/registry/food-app.json +1 -0
  109. package/registry/gift-card.json +1 -0
  110. package/registry/glass-button.json +5 -0
  111. package/registry/glass-nav-bar.json +1 -0
  112. package/registry/glass-search-bar.json +1 -0
  113. package/registry/glass-segmented.json +1 -0
  114. package/registry/glass-sheet.json +1 -0
  115. package/registry/glass-tab-bar.json +5 -0
  116. package/registry/glass.json +1 -0
  117. package/registry/grain/files/src/components/ui/grain.tsx +16 -14
  118. package/registry/grain.json +8 -6
  119. package/registry/inbox.json +1 -0
  120. package/registry/isometric-bar.json +1 -0
  121. package/registry/kit/files/src/lib/kit.tsx +363 -11
  122. package/registry/kit.json +1 -0
  123. package/registry/liquidglass-tab/files/src/app/_layout.tsx +0 -1
  124. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +23 -9
  125. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +2 -2
  126. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +1 -1
  127. package/registry/liquidglass-tab.json +1 -0
  128. package/registry/list-item/files/src/components/ui/list-item.tsx +42 -42
  129. package/registry/list-item/files/src/lib/kit.tsx +363 -11
  130. package/registry/list-item.json +1 -0
  131. package/registry/loyalty-card.json +1 -0
  132. package/registry/morphing-button-group.json +1 -0
  133. package/registry/number-ticker.json +1 -0
  134. package/registry/offers.json +1 -0
  135. package/registry/onboarding.json +1 -0
  136. package/registry/orb-reveal.json +1 -0
  137. package/registry/otp-input/files/src/components/ui/otp-input.tsx +48 -29
  138. package/registry/otp-input/files/src/lib/kit.tsx +363 -11
  139. package/registry/otp-input.json +1 -0
  140. package/registry/parallax-layers.json +1 -0
  141. package/registry/parallax-scroll.json +1 -0
  142. package/registry/password-input/files/src/components/ui/floating-input.tsx +33 -33
  143. package/registry/password-input/files/src/components/ui/password-input.tsx +29 -25
  144. package/registry/password-input/files/src/lib/kit.tsx +363 -11
  145. package/registry/password-input.json +1 -0
  146. package/registry/payment-card/files/src/components/ui/flip-card.tsx +6 -5
  147. package/registry/payment-card/files/src/components/ui/payment-card.tsx +17 -17
  148. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +76 -91
  149. package/registry/payment-card/files/src/lib/kit.tsx +363 -11
  150. package/registry/payment-card.json +1 -0
  151. package/registry/paywall.json +1 -0
  152. package/registry/pixel-bar.json +1 -0
  153. package/registry/polar-area.json +1 -0
  154. package/registry/profile.json +1 -0
  155. package/registry/progress/files/src/components/ui/progress.tsx +29 -29
  156. package/registry/progress/files/src/lib/kit.tsx +363 -11
  157. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +20 -20
  158. package/registry/progress-steps/files/src/lib/kit.tsx +363 -11
  159. package/registry/progress-steps.json +1 -0
  160. package/registry/progress.json +1 -0
  161. package/registry/qr-code-button.json +1 -0
  162. package/registry/radio-group/files/src/components/ui/radio-group.tsx +19 -19
  163. package/registry/radio-group/files/src/lib/kit.tsx +363 -11
  164. package/registry/radio-group.json +1 -0
  165. package/registry/rating.json +1 -0
  166. package/registry/ring-carousel.json +1 -0
  167. package/registry/ruler-time-picker/files/src/components/ui/ruler-time-picker.tsx +98 -82
  168. package/registry/ruler-time-picker/files/src/lib/kit.tsx +363 -11
  169. package/registry/ruler-time-picker.json +7 -0
  170. package/registry/scratch-card.json +1 -0
  171. package/registry/search-bar/files/src/components/ui/search-bar.tsx +25 -21
  172. package/registry/search-bar/files/src/lib/kit.tsx +363 -11
  173. package/registry/search-bar.json +1 -0
  174. package/registry/search-results.json +4 -0
  175. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +8 -7
  176. package/registry/segmented-control/files/src/lib/kit.tsx +363 -11
  177. package/registry/segmented-control.json +1 -0
  178. package/registry/select/files/src/components/ui/select.tsx +93 -88
  179. package/registry/select/files/src/lib/kit.tsx +363 -11
  180. package/registry/select.json +6 -0
  181. package/registry/settings.json +1 -0
  182. package/registry/sign-in.json +1 -0
  183. package/registry/skeleton/files/src/components/ui/skeleton.tsx +149 -80
  184. package/registry/skeleton/files/src/lib/kit.tsx +363 -11
  185. package/registry/skeleton.json +1 -0
  186. package/registry/slide-to-confirm.json +1 -0
  187. package/registry/slider/files/src/components/ui/slider.tsx +103 -96
  188. package/registry/slider/files/src/lib/kit.tsx +363 -11
  189. package/registry/slider.json +5 -0
  190. package/registry/spin-wheel.json +1 -0
  191. package/registry/spinner/files/src/components/ui/spinner.tsx +39 -29
  192. package/registry/spinner/files/src/lib/kit.tsx +363 -11
  193. package/registry/spinner.json +6 -0
  194. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +39 -32
  195. package/registry/stateful-button/files/src/lib/kit.tsx +363 -11
  196. package/registry/stateful-button.json +5 -0
  197. package/registry/stepper/files/src/components/ui/stepper.tsx +38 -36
  198. package/registry/stepper/files/src/lib/kit.tsx +363 -11
  199. package/registry/stepper.json +1 -0
  200. package/registry/swipe-deck.json +1 -0
  201. package/registry/swipeable-row.json +4 -0
  202. package/registry/switch/files/src/components/ui/switch.tsx +39 -42
  203. package/registry/switch/files/src/lib/kit.tsx +363 -11
  204. package/registry/switch.json +1 -0
  205. package/registry/tabs/files/src/components/ui/tabs.tsx +59 -62
  206. package/registry/tabs/files/src/lib/kit.tsx +363 -11
  207. package/registry/tabs.json +1 -0
  208. package/registry/text-animation/files/src/components/ui/text-animation.tsx +55 -42
  209. package/registry/text-animation/files/src/lib/kit.tsx +363 -11
  210. package/registry/text-animation.json +7 -0
  211. package/registry/textarea/files/src/components/ui/textarea.tsx +10 -10
  212. package/registry/textarea/files/src/lib/kit.tsx +363 -11
  213. package/registry/textarea.json +1 -0
  214. package/registry/ticket-card.json +1 -0
  215. package/registry/ticket-shape/files/src/lib/kit.tsx +363 -11
  216. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +54 -44
  217. package/registry/ticket-shape.json +1 -0
  218. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +76 -91
  219. package/registry/tilt-card/files/src/lib/kit.tsx +363 -11
  220. package/registry/tilt-card.json +1 -0
  221. package/registry/time-picker/files/src/components/ui/time-picker.tsx +122 -119
  222. package/registry/time-picker/files/src/lib/kit.tsx +363 -11
  223. package/registry/time-picker.json +1 -0
  224. package/registry/timeline/files/src/components/ui/avatar.tsx +84 -66
  225. package/registry/timeline/files/src/components/ui/timeline.tsx +44 -41
  226. package/registry/timeline/files/src/lib/kit.tsx +363 -11
  227. package/registry/timeline-chart.json +1 -0
  228. package/registry/timeline.json +1 -0
  229. package/registry/toast/files/src/components/ui/toast.tsx +541 -463
  230. package/registry/toast/files/src/lib/kit.tsx +363 -11
  231. package/registry/toast.json +7 -5
  232. package/registry/treemap.json +1 -0
  233. package/registry/waterfall.json +1 -0
  234. package/registry/wheel-list.json +1 -0
  235. package/schema/components.schema.json +1 -0
  236. package/registry/alert/files/src/components/ui/button.tsx +0 -290
  237. package/registry/confetti/files/src/lib/kit.tsx +0 -320
  238. package/registry/grain/files/src/lib/kit.tsx +0 -320
  239. package/registry/toast/files/src/components/ui/button.tsx +0 -290
@@ -1,12 +1,11 @@
1
1
  import { createAudioPlayer, type AudioPlayer } from 'expo-audio';
2
2
  import { File, Paths } from 'expo-file-system';
3
3
  import * as Haptics from 'expo-haptics';
4
- import React, { useEffect, useMemo, useRef, useState } from 'react';
4
+ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
5
5
  import {
6
6
  AccessibilityInfo,
7
7
  Animated,
8
8
  Easing,
9
- PanResponder,
10
9
  Platform,
11
10
  StyleSheet,
12
11
  Text,
@@ -18,7 +17,7 @@ import {
18
17
  type ViewStyle,
19
18
  } from 'react-native';
20
19
 
21
- import { motion, useKit } from '@/lib/kit';
20
+ import { motion, useKit, usePanResponder } from '@/lib/kit';
22
21
 
23
22
  /** A wall-clock time, 24-hour: `{ hours: 0–23, minutes: 0–59 }`. */
24
23
  export interface TimeValue {
@@ -116,12 +115,19 @@ export function stepTime(base: number, ticks: number, step: number) {
116
115
  return mod(base + ticks * step, DAY);
117
116
  }
118
117
 
119
- /** `'02:15 am'` / `'14:15'` split into the glyphs the picker rolls. */
120
- export function timeGlyphs(minutes: number, hour12: boolean) {
118
+ /**
119
+ * `'02:15 am'` / `'14:15'` split into the glyphs the picker rolls. `period` is the key ('am' / 'pm');
120
+ * the spoken label names it with `periods` (default English, pass `useKit().labels` to localise).
121
+ */
122
+ export function timeGlyphs(minutes: number, hour12: boolean, periods: { am: string; pm: string } = { am: 'AM', pm: 'PM' }) {
121
123
  const { hours, minutes: m } = fromMinutes(minutes);
122
124
  const h = hour12 ? hours % 12 || 12 : hours;
123
125
  const digits = pad2(h) + pad2(m);
124
- return { digits: digits.split(''), period: hour12 ? (hours < 12 ? 'am' : 'pm') : null, label: `${pad2(h)}:${pad2(m)}${hour12 ? (hours < 12 ? ' AM' : ' PM') : ''}` };
126
+ return {
127
+ digits: digits.split(''),
128
+ period: hour12 ? (hours < 12 ? ('am' as const) : ('pm' as const)) : null,
129
+ label: `${pad2(h)}:${pad2(m)}${hour12 ? ` ${hours < 12 ? periods.am : periods.pm}` : ''}`,
130
+ };
125
131
  }
126
132
 
127
133
  export const dayOf = (date: Date): Day => ({ y: date.getFullYear(), m: date.getMonth(), d: date.getDate() });
@@ -188,9 +194,9 @@ export function dateParts(t: Day, showYear: 'auto' | 'always' | 'never' = 'auto'
188
194
  const wd = new Date(utc(t)).getUTCDay();
189
195
  const year = showYear === 'always' || (showYear === 'auto' && t.y !== thisYear) ? String(t.y) : null;
190
196
  return {
191
- weekday: n.days[wd],
197
+ weekday: n.days[wd] ?? '',
192
198
  day: String(t.d).split(''),
193
- month: n.months[t.m],
199
+ month: n.months[t.m] ?? '',
194
200
  year,
195
201
  label: `${n.daysLong[wd]}, ${t.d} ${n.monthsLong[t.m]} ${t.y}`,
196
202
  };
@@ -207,13 +213,16 @@ export function RulerTimePicker(props: RulerTimePickerProps) {
207
213
  }
208
214
 
209
215
  function TimeRuler({ value, defaultValue, onChange, label, step: rawStep = 5, hour12 = true, size = 'md', accent, haptics = true, sound = true, accessibilityLabel, style, textStyle }: RulerTimeProps) {
216
+ const { labels } = useKit();
210
217
  const step = Math.max(1, Math.min(60, Math.round(rawStep) || 5));
211
218
  const initial = toMinutes(value ?? defaultValue ?? { hours: 9, minutes: 0 });
212
219
  // Minutes shown, and which way the last change went (rolls go up for later, down for earlier).
213
220
  const [shown, setShown] = useState({ minutes: initial, dir: 1 as 1 | -1 });
214
221
  const base = useRef(initial);
215
222
  const live = useRef({ onChange, step, shown: initial });
216
- live.current = { onChange, step, shown: shown.minutes };
223
+ useLayoutEffect(() => {
224
+ live.current = { onChange, step, shown: shown.minutes };
225
+ });
217
226
 
218
227
  const ruler = useRuler({
219
228
  gap: TIME_GAP,
@@ -242,20 +251,22 @@ function TimeRuler({ value, defaultValue, onChange, label, step: rawStep = 5, ho
242
251
 
243
252
  const fontSize = TIME_SIZES[size];
244
253
  const { box, glyph } = useGlyph(fontSize, textStyle);
245
- const t = timeGlyphs(shown.minutes, hour12);
254
+ const t = timeGlyphs(shown.minutes, hour12, labels);
255
+ const [h1 = '', h2 = '', m1 = '', m2 = ''] = t.digits;
246
256
  const roll = { dir: shown.dir, height: box, style: glyph, reduce: ruler.reduce };
247
257
 
248
258
  return (
249
- <Frame ruler={ruler} label={label} name={accessibilityLabel ?? label?.replace(/:\s*$/, '') ?? 'Time'} spoken={t.label} accent={accent} rulerWidth={fontSize * 5.6} style={style}>
259
+ <Frame ruler={ruler} label={label} name={accessibilityLabel ?? label?.replace(/:\s*$/, '') ?? labels.time} spoken={t.label} accent={accent} rulerWidth={fontSize * 5.6} style={style}>
250
260
  <View style={[styles.row, { height: box }]}>
251
- <Roll text={t.digits[0]} {...roll} />
252
- <Roll text={t.digits[1]} {...roll} />
261
+ <Roll text={h1} {...roll} />
262
+ <Roll text={h2} {...roll} />
253
263
  <Text maxFontSizeMultiplier={FONT_CAP} style={glyph}>:</Text>
254
- <Roll text={t.digits[2]} {...roll} />
255
- <Roll text={t.digits[3]} {...roll} />
264
+ <Roll text={m1} {...roll} />
265
+ <Roll text={m2} {...roll} />
256
266
  {t.period ? (
257
267
  <View style={{ marginLeft: fontSize * 0.24 }}>
258
- <Roll text={t.period} {...roll} />
268
+ {/* Shown lowercase, as the design has it ('am' / 'pm'); the text comes from the kit labels. */}
269
+ <Roll text={labels[t.period].toLowerCase()} {...roll} />
259
270
  </View>
260
271
  ) : null}
261
272
  </View>
@@ -264,17 +275,20 @@ function TimeRuler({ value, defaultValue, onChange, label, step: rawStep = 5, ho
264
275
  }
265
276
 
266
277
  function DateRuler({ value, defaultValue, onChange, label, minDate, maxDate, showYear = 'auto', locale, size = 'md', accent, haptics = true, sound = true, accessibilityLabel, style, textStyle }: RulerDateProps) {
267
- const start = useRef(value ?? defaultValue ?? new Date()).current;
278
+ const [start] = useState(() => value ?? defaultValue ?? new Date());
268
279
  // Time of day handed back with every change, so picking a date never moves a set time.
269
280
  const clock = useRef({ h: start.getHours(), min: start.getMinutes() });
270
281
  const [shown, setShown] = useState({ day: dayOf(start), index: 0, dir: 1 as 1 | -1 });
271
- const base = useRef(dayOf(start));
282
+ // The day at tick 0; a controlled value from outside re-bases the ruler on it.
283
+ const [base, setBase] = useState(() => dayOf(start));
272
284
  const live = useRef({ onChange, key: utc(shown.day) });
273
- live.current = { onChange, key: utc(shown.day) };
285
+ useLayoutEffect(() => {
286
+ live.current = { onChange, key: utc(shown.day) };
287
+ });
274
288
 
275
289
  // Limits as offsets from the base day.
276
- const lo = minDate ? daysBetween(base.current, dayOf(minDate)) : -Infinity;
277
- const hi = maxDate ? Math.max(lo, daysBetween(base.current, dayOf(maxDate))) : Infinity;
290
+ const lo = minDate ? daysBetween(base, dayOf(minDate)) : -Infinity;
291
+ const hi = maxDate ? Math.max(lo, daysBetween(base, dayOf(maxDate))) : Infinity;
278
292
 
279
293
  const ruler = useRuler({
280
294
  gap: DATE_GAP,
@@ -282,9 +296,9 @@ function DateRuler({ value, defaultValue, onChange, label, minDate, maxDate, sho
282
296
  lo,
283
297
  hi,
284
298
  sound,
285
- firm: (i) => addDays(base.current, i).d === 1,
299
+ firm: (i) => addDays(base, i).d === 1,
286
300
  onStep: (i, dir) => {
287
- const day = addDays(base.current, i);
301
+ const day = addDays(base, i);
288
302
  live.current.key = utc(day);
289
303
  setShown({ day, index: i, dir });
290
304
  live.current.onChange?.(new Date(day.y, day.m, day.d, clock.current.h, clock.current.min));
@@ -298,14 +312,14 @@ function DateRuler({ value, defaultValue, onChange, label, minDate, maxDate, sho
298
312
  const day = dayOf(value);
299
313
  clock.current = { h: value.getHours(), min: value.getMinutes() };
300
314
  const dir = external > live.current.key ? 1 : -1;
301
- base.current = day;
315
+ setBase(day);
302
316
  live.current.key = external;
303
317
  ruler.reset();
304
318
  setShown({ day, index: 0, dir });
305
319
  // eslint-disable-next-line react-hooks/exhaustive-deps
306
320
  }, [external]);
307
321
 
308
- const { colors, font } = useKit();
322
+ const { colors, font, labels } = useKit();
309
323
  const fontSize = DATE_SIZES[size];
310
324
  const { box, glyph } = useGlyph(fontSize, textStyle);
311
325
  const gap = fontSize * 0.24;
@@ -317,18 +331,17 @@ function DateRuler({ value, defaultValue, onChange, label, minDate, maxDate, sho
317
331
  const reach = ruler.width ? Math.ceil(ruler.width / 2 / DATE_GAP) + 45 : 0;
318
332
  const months = useMemo(() => {
319
333
  if (!reach) return [];
320
- return monthStarts(base.current, shown.index - reach, shown.index + reach).map((i) => {
321
- const t = addDays(base.current, i);
322
- return { i, name: t.m === 0 ? `${names.months[0]} ${String(t.y).slice(2)}` : names.months[t.m] };
334
+ return monthStarts(base, shown.index - reach, shown.index + reach).map((i) => {
335
+ const t = addDays(base, i);
336
+ return { i, name: t.m === 0 ? `${names.months[0] ?? ''} ${String(t.y).slice(2)}` : (names.months[t.m] ?? '') };
323
337
  });
324
- // eslint-disable-next-line react-hooks/exhaustive-deps
325
- }, [shown.index, reach, names, external]);
338
+ }, [shown.index, reach, names, base]);
326
339
 
327
340
  return (
328
341
  <Frame
329
342
  ruler={ruler}
330
343
  label={label}
331
- name={accessibilityLabel ?? label?.replace(/:\s*$/, '') ?? 'Date'}
344
+ name={accessibilityLabel ?? label?.replace(/:\s*$/, '') ?? labels.date}
332
345
  spoken={p.label}
333
346
  accent={accent}
334
347
  rulerWidth={fontSize * 7}
@@ -381,14 +394,16 @@ interface RulerOptions {
381
394
  function useRuler({ gap, haptics, sound, lo = -Infinity, hi = Infinity, firm, onStep }: RulerOptions) {
382
395
  const reduce = useReduceMotion();
383
396
  const tick = useTickSound(sound);
384
- const pos = useRef(new Animated.Value(0)).current;
385
- const lag = useRef(new Animated.Value(0)).current;
397
+ const [pos] = useState(() => new Animated.Value(0));
398
+ const [lag] = useState(() => new Animated.Value(0));
386
399
  const index = useRef(0);
387
400
  const posNow = useRef(0);
388
401
  const moving = useRef(false);
389
402
  const flinging = useRef(false);
390
403
  const live = useRef({ gap, haptics, lo, hi, firm, onStep, tick });
391
- live.current = { gap, haptics, lo, hi, firm, onStep, tick };
404
+ useLayoutEffect(() => {
405
+ live.current = { gap, haptics, lo, hi, firm, onStep, tick };
406
+ });
392
407
  const lagMax = gap * 2.5;
393
408
 
394
409
  const snap = (to: number) => {
@@ -398,7 +413,9 @@ function useRuler({ gap, haptics, sound, lo = -Infinity, hi = Infinity, firm, on
398
413
  });
399
414
  };
400
415
  const snapRef = useRef(snap);
401
- snapRef.current = snap;
416
+ useLayoutEffect(() => {
417
+ snapRef.current = snap;
418
+ });
402
419
  const settle = (v: number) => clamp(Math.round(v), live.current.lo, live.current.hi);
403
420
 
404
421
  useEffect(() => {
@@ -428,46 +445,43 @@ function useRuler({ gap, haptics, sound, lo = -Infinity, hi = Infinity, firm, on
428
445
  // Where the ruler was when the finger came down (g.dx counts from the touch, so the ruler stays
429
446
  // under the finger even though a sideways drag is claimed a few points in).
430
447
  const drag = useRef(0);
431
- const pan = useMemo(() => {
432
- const unlag = () => Animated.spring(lag, { toValue: 0, useNativeDriver: true, ...motion.gentle }).start();
433
- return PanResponder.create({
434
- // A touch only claims the ruler straight away to catch a fling; otherwise a sideways drag does.
435
- onStartShouldSetPanResponder: () => moving.current,
436
- onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 4 && Math.abs(g.dx) > Math.abs(g.dy),
437
- onPanResponderTerminationRequest: () => false,
438
- onPanResponderGrant: (_, g) => {
448
+ const unlag = () => Animated.spring(lag, { toValue: 0, useNativeDriver: true, ...motion.gentle }).start();
449
+ const pan = usePanResponder({
450
+ // A touch only claims the ruler straight away to catch a fling; otherwise a sideways drag does.
451
+ onStartShouldSetPanResponder: () => moving.current,
452
+ onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 4 && Math.abs(g.dx) > Math.abs(g.dy),
453
+ onPanResponderTerminationRequest: () => false,
454
+ onPanResponderGrant: (_, g) => {
455
+ flinging.current = false;
456
+ pos.stopAnimation((v) => (posNow.current = v));
457
+ lag.stopAnimation();
458
+ moving.current = true;
459
+ drag.current = posNow.current + g.dx / live.current.gap;
460
+ },
461
+ onPanResponderMove: (_, g) => {
462
+ const { lo: min, hi: max, gap: px } = live.current;
463
+ const raw = drag.current - g.dx / px;
464
+ pos.setValue(raw < min ? min - (min - raw) * RUBBER : raw > max ? max + (raw - max) * RUBBER : raw);
465
+ lag.setValue(clamp(-g.vx * 14, -lagMax, lagMax));
466
+ },
467
+ onPanResponderRelease: (_, g) => {
468
+ unlag();
469
+ const { lo: min, hi: max, gap: px } = live.current;
470
+ const velocity = -g.vx / px; // ticks per ms
471
+ const outside = posNow.current < min || posNow.current > max;
472
+ if (outside || Math.abs(velocity) < 0.03) return snapRef.current(settle(posNow.current));
473
+ flinging.current = true;
474
+ Animated.decay(pos, { velocity, deceleration: 0.993, useNativeDriver: true }).start(({ finished }) => {
475
+ if (!finished || !flinging.current) return;
439
476
  flinging.current = false;
440
- pos.stopAnimation((v) => (posNow.current = v));
441
- lag.stopAnimation();
442
- moving.current = true;
443
- drag.current = posNow.current + g.dx / live.current.gap;
444
- },
445
- onPanResponderMove: (_, g) => {
446
- const { lo: min, hi: max, gap: px } = live.current;
447
- const raw = drag.current - g.dx / px;
448
- pos.setValue(raw < min ? min - (min - raw) * RUBBER : raw > max ? max + (raw - max) * RUBBER : raw);
449
- lag.setValue(clamp(-g.vx * 14, -lagMax, lagMax));
450
- },
451
- onPanResponderRelease: (_, g) => {
452
- unlag();
453
- const { lo: min, hi: max, gap: px } = live.current;
454
- const velocity = -g.vx / px; // ticks per ms
455
- const outside = posNow.current < min || posNow.current > max;
456
- if (outside || Math.abs(velocity) < 0.03) return snapRef.current(settle(posNow.current));
457
- flinging.current = true;
458
- Animated.decay(pos, { velocity, deceleration: 0.993, useNativeDriver: true }).start(({ finished }) => {
459
- if (!finished || !flinging.current) return;
460
- flinging.current = false;
461
- pos.stopAnimation((v) => snapRef.current(settle(v)));
462
- });
463
- },
464
- onPanResponderTerminate: () => {
465
- unlag();
466
- snapRef.current(settle(posNow.current));
467
- },
468
- });
469
- // eslint-disable-next-line react-hooks/exhaustive-deps
470
- }, [pos, lag]);
477
+ pos.stopAnimation((v) => snapRef.current(settle(v)));
478
+ });
479
+ },
480
+ onPanResponderTerminate: () => {
481
+ unlag();
482
+ snapRef.current(settle(posNow.current));
483
+ },
484
+ });
471
485
 
472
486
  const [width, setWidth] = useState(0);
473
487
  return {
@@ -515,15 +529,15 @@ function useGlyph(fontSize: number, textStyle?: StyleProp<TextStyle>) {
515
529
 
516
530
  interface FrameProps {
517
531
  ruler: Ruler;
518
- label?: string;
532
+ label?: string | undefined;
519
533
  name: string;
520
534
  spoken: string;
521
- accent?: string;
535
+ accent?: string | undefined;
522
536
  rulerWidth: number;
523
537
  /** Extra height under the ticks (month labels). */
524
- extra?: number;
538
+ extra?: number | undefined;
525
539
  marks?: React.ReactNode;
526
- style?: StyleProp<ViewStyle>;
540
+ style?: StyleProp<ViewStyle> | undefined;
527
541
  children: React.ReactNode;
528
542
  }
529
543
 
@@ -540,7 +554,7 @@ function Frame({ ruler, label, name, spoken, accent, rulerWidth, extra = 0, mark
540
554
  accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
541
555
  onAccessibilityAction={(e) => ruler.nudge(e.nativeEvent.actionName === 'increment' ? 1 : -1)}
542
556
  style={[styles.root, styles.grab, style]}
543
- {...ruler.pan.panHandlers}
557
+ {...ruler.pan}
544
558
  >
545
559
  {label ? <Text style={[styles.label, { color: colors.foreground, fontFamily: font.family }]}>{label}</Text> : null}
546
560
  {children}
@@ -561,7 +575,8 @@ function Frame({ ruler, label, name, spoken, accent, rulerWidth, extra = 0, mark
561
575
  }
562
576
 
563
577
  /** Ticks fade out toward both ends of the ruler. */
564
- function fade(x: Animated.AnimatedInterpolation<number> | Animated.AnimatedMultiplication<number>, half: number, gap: number) {
578
+ // ReturnType keeps this compiling on React Native 0.86 (generic AnimatedMultiplication) and 0.88+ (not generic).
579
+ function fade(x: Animated.AnimatedInterpolation<number> | ReturnType<typeof Animated.multiply>, half: number, gap: number) {
565
580
  return x.interpolate({
566
581
  inputRange: [-half, -half * 0.55, -gap * 2, 0, gap * 2, half * 0.55, half],
567
582
  outputRange: [0, 0.35, 0.85, 1, 0.85, 0.35, 0],
@@ -622,7 +637,7 @@ interface MonthMarksProps {
622
637
  months: { i: number; name: string }[];
623
638
  tick: string;
624
639
  text: string;
625
- family?: string;
640
+ family?: string | undefined;
626
641
  }
627
642
 
628
643
  /** A taller tick and the month's name under the 1st of each month, riding the ruler natively. */
@@ -669,7 +684,7 @@ const GHOSTS: [number, number][] = Array.from({ length: 7 }, (_, i) => {
669
684
  * the other side, both smeared by ghost copies that fade as the roll settles.
670
685
  */
671
686
  const Roll = React.memo(function Roll({ text, dir, height, style, reduce }: RollProps) {
672
- const t = useRef(new Animated.Value(1)).current;
687
+ const [t] = useState(() => new Animated.Value(1));
673
688
  const last = useRef(text);
674
689
  const [pair, setPair] = useState({ prev: text, cur: text, dir });
675
690
 
@@ -776,6 +791,7 @@ function useTickSound(enabled: boolean) {
776
791
  if (!pool.current.length || now - state.current.last < 25) return;
777
792
  state.current.last = now;
778
793
  const player = pool.current[state.current.next++ % pool.current.length];
794
+ if (!player) return;
779
795
  player.volume = firm ? 0.9 : 0.5;
780
796
  player.seekTo(0).catch(() => {});
781
797
  player.play();