panelui-native 0.73.0 → 0.75.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 (192) hide show
  1. package/README.md +3 -1
  2. package/lib/module/components/avatar/avatar-group.js +64 -0
  3. package/lib/module/components/avatar/avatar-group.js.map +1 -0
  4. package/lib/module/components/avatar/index.js +84 -2
  5. package/lib/module/components/avatar/index.js.map +1 -1
  6. package/lib/module/components/color-picker/color-picker-accessibility.js +12 -0
  7. package/lib/module/components/color-picker/color-picker-accessibility.js.map +1 -0
  8. package/lib/module/components/color-picker/index.js +9 -5
  9. package/lib/module/components/color-picker/index.js.map +1 -1
  10. package/lib/module/components/date-time-picker/index.js +11 -3
  11. package/lib/module/components/date-time-picker/index.js.map +1 -1
  12. package/lib/module/components/drawer/index.js +62 -13
  13. package/lib/module/components/drawer/index.js.map +1 -1
  14. package/lib/module/components/input/index.js +7 -4
  15. package/lib/module/components/input/index.js.map +1 -1
  16. package/lib/module/components/input/input-content-padding.js +6 -0
  17. package/lib/module/components/input/input-content-padding.js.map +1 -0
  18. package/lib/module/components/input-group/index.js +14 -9
  19. package/lib/module/components/input-group/index.js.map +1 -1
  20. package/lib/module/components/input-group/input-group-measurements.js +60 -0
  21. package/lib/module/components/input-group/input-group-measurements.js.map +1 -0
  22. package/lib/module/components/otp-input/index.js +3 -1
  23. package/lib/module/components/otp-input/index.js.map +1 -1
  24. package/lib/module/components/otp-input/otp-input-state.js +7 -0
  25. package/lib/module/components/otp-input/otp-input-state.js.map +1 -0
  26. package/lib/module/components/planner/index.js +20 -6
  27. package/lib/module/components/planner/index.js.map +1 -1
  28. package/lib/module/components/planner/planner-today.js +8 -0
  29. package/lib/module/components/planner/planner-today.js.map +1 -0
  30. package/lib/module/components/plot/index.js +8 -11
  31. package/lib/module/components/plot/index.js.map +1 -1
  32. package/lib/module/components/plot/plot-series-registry.js +32 -0
  33. package/lib/module/components/plot/plot-series-registry.js.map +1 -0
  34. package/lib/module/components/radio-group/index.js +2 -1
  35. package/lib/module/components/radio-group/index.js.map +1 -1
  36. package/lib/module/components/radio-group/radio-group-value.js +8 -0
  37. package/lib/module/components/radio-group/radio-group-value.js.map +1 -0
  38. package/lib/module/components/rating/index.js +7 -4
  39. package/lib/module/components/rating/index.js.map +1 -1
  40. package/lib/module/components/rating/rating-inputs.js +15 -0
  41. package/lib/module/components/rating/rating-inputs.js.map +1 -0
  42. package/lib/module/components/search-bar/index.js +7 -7
  43. package/lib/module/components/search-bar/index.js.map +1 -1
  44. package/lib/module/components/search-bar/search-bar-debounce.js +23 -0
  45. package/lib/module/components/search-bar/search-bar-debounce.js.map +1 -0
  46. package/lib/module/components/select/index.js +9 -1
  47. package/lib/module/components/select/index.js.map +1 -1
  48. package/lib/module/components/select/native-select-contract.js +11 -0
  49. package/lib/module/components/select/native-select-contract.js.map +1 -0
  50. package/lib/module/components/split-view/index.js +439 -0
  51. package/lib/module/components/split-view/index.js.map +1 -0
  52. package/lib/module/components/split-view/split-view-math.js +132 -0
  53. package/lib/module/components/split-view/split-view-math.js.map +1 -0
  54. package/lib/module/components/splitter/index.js +433 -0
  55. package/lib/module/components/splitter/index.js.map +1 -0
  56. package/lib/module/components/splitter/splitter-math.js +206 -0
  57. package/lib/module/components/splitter/splitter-math.js.map +1 -0
  58. package/lib/module/components/steps/index.js +16 -5
  59. package/lib/module/components/steps/index.js.map +1 -1
  60. package/lib/module/components/steps/steps-trigger.js +6 -0
  61. package/lib/module/components/steps/steps-trigger.js.map +1 -0
  62. package/lib/module/components/tag-input/index.js +15 -10
  63. package/lib/module/components/tag-input/index.js.map +1 -1
  64. package/lib/module/components/tag-input/tag-input-state.js +14 -0
  65. package/lib/module/components/tag-input/tag-input-state.js.map +1 -0
  66. package/lib/module/components/textarea/index.js +9 -4
  67. package/lib/module/components/textarea/index.js.map +1 -1
  68. package/lib/module/components/textarea/textarea-disabled.js +6 -0
  69. package/lib/module/components/textarea/textarea-disabled.js.map +1 -0
  70. package/lib/module/components/timeline/index.js +83 -27
  71. package/lib/module/components/timeline/index.js.map +1 -1
  72. package/lib/module/components/timeline/timeline-geometry.js +11 -0
  73. package/lib/module/components/timeline/timeline-geometry.js.map +1 -0
  74. package/lib/module/components/toast/index.js +18 -1
  75. package/lib/module/components/toast/index.js.map +1 -1
  76. package/lib/module/components/toast/toast-store.js +36 -4
  77. package/lib/module/components/toast/toast-store.js.map +1 -1
  78. package/lib/module/components/tour/index.js +4 -2
  79. package/lib/module/components/tour/index.js.map +1 -1
  80. package/lib/module/components/tour/tour-card-measurement.js +24 -0
  81. package/lib/module/components/tour/tour-card-measurement.js.map +1 -0
  82. package/lib/module/index.js +2 -0
  83. package/lib/module/index.js.map +1 -1
  84. package/lib/typescript/src/components/avatar/avatar-group.d.ts +46 -0
  85. package/lib/typescript/src/components/avatar/avatar-group.d.ts.map +1 -0
  86. package/lib/typescript/src/components/avatar/index.d.ts +23 -1
  87. package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
  88. package/lib/typescript/src/components/color-picker/color-picker-accessibility.d.ts +8 -0
  89. package/lib/typescript/src/components/color-picker/color-picker-accessibility.d.ts.map +1 -0
  90. package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -1
  91. package/lib/typescript/src/components/date-time-picker/index.d.ts.map +1 -1
  92. package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
  93. package/lib/typescript/src/components/input/index.d.ts.map +1 -1
  94. package/lib/typescript/src/components/input/input-content-padding.d.ts +2 -0
  95. package/lib/typescript/src/components/input/input-content-padding.d.ts.map +1 -0
  96. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
  97. package/lib/typescript/src/components/input-group/input-group-measurements.d.ts +16 -0
  98. package/lib/typescript/src/components/input-group/input-group-measurements.d.ts.map +1 -0
  99. package/lib/typescript/src/components/otp-input/index.d.ts.map +1 -1
  100. package/lib/typescript/src/components/otp-input/otp-input-state.d.ts +3 -0
  101. package/lib/typescript/src/components/otp-input/otp-input-state.d.ts.map +1 -0
  102. package/lib/typescript/src/components/planner/index.d.ts.map +1 -1
  103. package/lib/typescript/src/components/planner/planner-today.d.ts +2 -0
  104. package/lib/typescript/src/components/planner/planner-today.d.ts.map +1 -0
  105. package/lib/typescript/src/components/plot/index.d.ts +1 -1
  106. package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
  107. package/lib/typescript/src/components/plot/plot-series-registry.d.ts +8 -0
  108. package/lib/typescript/src/components/plot/plot-series-registry.d.ts.map +1 -0
  109. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  110. package/lib/typescript/src/components/radio-group/radio-group-value.d.ts +2 -0
  111. package/lib/typescript/src/components/radio-group/radio-group-value.d.ts.map +1 -0
  112. package/lib/typescript/src/components/rating/index.d.ts.map +1 -1
  113. package/lib/typescript/src/components/rating/rating-inputs.d.ts +4 -0
  114. package/lib/typescript/src/components/rating/rating-inputs.d.ts.map +1 -0
  115. package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -1
  116. package/lib/typescript/src/components/search-bar/search-bar-debounce.d.ts +7 -0
  117. package/lib/typescript/src/components/search-bar/search-bar-debounce.d.ts.map +1 -0
  118. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  119. package/lib/typescript/src/components/select/native-select-contract.d.ts +11 -0
  120. package/lib/typescript/src/components/select/native-select-contract.d.ts.map +1 -0
  121. package/lib/typescript/src/components/split-view/index.d.ts +159 -0
  122. package/lib/typescript/src/components/split-view/index.d.ts.map +1 -0
  123. package/lib/typescript/src/components/split-view/split-view-math.d.ts +54 -0
  124. package/lib/typescript/src/components/split-view/split-view-math.d.ts.map +1 -0
  125. package/lib/typescript/src/components/splitter/index.d.ts +78 -0
  126. package/lib/typescript/src/components/splitter/index.d.ts.map +1 -0
  127. package/lib/typescript/src/components/splitter/splitter-math.d.ts +67 -0
  128. package/lib/typescript/src/components/splitter/splitter-math.d.ts.map +1 -0
  129. package/lib/typescript/src/components/steps/index.d.ts +2 -2
  130. package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
  131. package/lib/typescript/src/components/steps/steps-trigger.d.ts +2 -0
  132. package/lib/typescript/src/components/steps/steps-trigger.d.ts.map +1 -0
  133. package/lib/typescript/src/components/tag-input/index.d.ts.map +1 -1
  134. package/lib/typescript/src/components/tag-input/tag-input-state.d.ts +12 -0
  135. package/lib/typescript/src/components/tag-input/tag-input-state.d.ts.map +1 -0
  136. package/lib/typescript/src/components/textarea/index.d.ts.map +1 -1
  137. package/lib/typescript/src/components/textarea/textarea-disabled.d.ts +2 -0
  138. package/lib/typescript/src/components/textarea/textarea-disabled.d.ts.map +1 -0
  139. package/lib/typescript/src/components/timeline/index.d.ts +1 -0
  140. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  141. package/lib/typescript/src/components/timeline/timeline-geometry.d.ts +4 -0
  142. package/lib/typescript/src/components/timeline/timeline-geometry.d.ts.map +1 -0
  143. package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
  144. package/lib/typescript/src/components/toast/toast-store.d.ts +7 -2
  145. package/lib/typescript/src/components/toast/toast-store.d.ts.map +1 -1
  146. package/lib/typescript/src/components/tour/index.d.ts.map +1 -1
  147. package/lib/typescript/src/components/tour/tour-card-measurement.d.ts +14 -0
  148. package/lib/typescript/src/components/tour/tour-card-measurement.d.ts.map +1 -0
  149. package/lib/typescript/src/index.d.ts +3 -1
  150. package/lib/typescript/src/index.d.ts.map +1 -1
  151. package/package.json +1 -1
  152. package/src/components/avatar/avatar-group.ts +78 -0
  153. package/src/components/avatar/index.tsx +121 -1
  154. package/src/components/color-picker/color-picker-accessibility.ts +12 -0
  155. package/src/components/color-picker/index.tsx +27 -8
  156. package/src/components/date-time-picker/index.tsx +11 -3
  157. package/src/components/drawer/index.tsx +47 -15
  158. package/src/components/input/index.tsx +6 -2
  159. package/src/components/input/input-content-padding.ts +3 -0
  160. package/src/components/input-group/index.tsx +38 -10
  161. package/src/components/input-group/input-group-measurements.ts +98 -0
  162. package/src/components/otp-input/index.tsx +3 -1
  163. package/src/components/otp-input/otp-input-state.ts +7 -0
  164. package/src/components/planner/index.tsx +24 -6
  165. package/src/components/planner/planner-today.ts +5 -0
  166. package/src/components/plot/index.tsx +16 -12
  167. package/src/components/plot/plot-series-registry.ts +41 -0
  168. package/src/components/radio-group/index.tsx +4 -1
  169. package/src/components/radio-group/radio-group-value.ts +9 -0
  170. package/src/components/rating/index.tsx +11 -4
  171. package/src/components/rating/rating-inputs.ts +19 -0
  172. package/src/components/search-bar/index.tsx +12 -7
  173. package/src/components/search-bar/search-bar-debounce.ts +35 -0
  174. package/src/components/select/index.tsx +10 -1
  175. package/src/components/select/native-select-contract.ts +12 -0
  176. package/src/components/split-view/index.tsx +565 -0
  177. package/src/components/split-view/split-view-math.ts +146 -0
  178. package/src/components/splitter/index.tsx +589 -0
  179. package/src/components/splitter/splitter-math.ts +244 -0
  180. package/src/components/steps/index.tsx +35 -9
  181. package/src/components/steps/steps-trigger.ts +6 -0
  182. package/src/components/tag-input/index.tsx +13 -11
  183. package/src/components/tag-input/tag-input-state.ts +18 -0
  184. package/src/components/textarea/index.tsx +8 -4
  185. package/src/components/textarea/textarea-disabled.ts +6 -0
  186. package/src/components/timeline/index.tsx +89 -26
  187. package/src/components/timeline/timeline-geometry.ts +9 -0
  188. package/src/components/toast/index.tsx +26 -1
  189. package/src/components/toast/toast-store.ts +43 -8
  190. package/src/components/tour/index.tsx +11 -3
  191. package/src/components/tour/tour-card-measurement.ts +29 -0
  192. package/src/index.ts +17 -0
@@ -38,6 +38,7 @@ import Animated, {
38
38
  import { tv, type VariantProps } from 'tailwind-variants';
39
39
  import { useCSSVariable } from 'uniwind';
40
40
  import { Text } from '../../primitives/text';
41
+ import { resolveOtpEditable } from './otp-input-state';
41
42
 
42
43
  /** Long enough to read as a transition, short enough not to lag a fast type. */
43
44
  const FOCUS_DURATION = 150;
@@ -245,6 +246,7 @@ export const OtpInput = forwardRef<TextInput, OtpInputProps>(
245
246
  groupEvery = 0,
246
247
  size,
247
248
  disabled,
249
+ editable,
248
250
  isInvalid,
249
251
  errorMessage,
250
252
  accessibilityLabel,
@@ -351,7 +353,7 @@ export const OtpInput = forwardRef<TextInput, OtpInputProps>(
351
353
  ref={setRef}
352
354
  value={value}
353
355
  onChangeText={handleChange}
354
- editable={!disabled}
356
+ editable={resolveOtpEditable(editable, disabled)}
355
357
  keyboardType={type === 'numeric' ? 'number-pad' : 'default'}
356
358
  // The OS one-time-code affordances: iOS surfaces the SMS code above
357
359
  // the keyboard, Android offers to autofill it.
@@ -0,0 +1,7 @@
1
+ /** Resolve the native editing state without allowing `disabled` to be bypassed. */
2
+ export function resolveOtpEditable(
3
+ editable: boolean | undefined,
4
+ disabled: boolean | undefined
5
+ ): boolean {
6
+ return editable !== false && !disabled;
7
+ }
@@ -115,6 +115,7 @@ import {
115
115
  usePlannerMonthLifecycle,
116
116
  usePlannerSelectionLifecycle,
117
117
  } from './planner-lifecycle';
118
+ import { millisecondsUntilNextLocalDay } from './planner-today';
118
119
 
119
120
  /** How many of a day's entries a cell draws before it stops and counts. */
120
121
  const DEFAULT_ENTRY_LIMIT = 2;
@@ -249,21 +250,38 @@ function usePlanner(component: string): PlannerContextValue {
249
250
  return context;
250
251
  }
251
252
 
252
- /** Today, recomputed when the app comes back rather than only at mount. */
253
+ /** Today, recomputed at local midnight and whenever the app comes back. */
253
254
  function useToday(): Date {
254
255
  const [today, setToday] = useState(() => startOfDay(new Date()));
255
256
 
256
257
  useEffect(() => {
257
- const refresh = () => {
258
- const now = startOfDay(new Date());
258
+ let timer: ReturnType<typeof setTimeout> | null = null;
259
+
260
+ const clearTimer = () => {
261
+ if (timer === null) return;
262
+ clearTimeout(timer);
263
+ timer = null;
264
+ };
265
+
266
+ const refreshAndSchedule = () => {
267
+ const current = new Date();
268
+ const now = startOfDay(current);
259
269
  // A new object every resume would invalidate every memo below it for a
260
270
  // date that has not changed.
261
- setToday((current) => (current.getTime() === now.getTime() ? current : now));
271
+ setToday((previous) => (previous.getTime() === now.getTime() ? previous : now));
272
+ clearTimer();
273
+ timer = setTimeout(refreshAndSchedule, millisecondsUntilNextLocalDay(current));
262
274
  };
275
+
276
+ refreshAndSchedule();
263
277
  const subscription = AppState.addEventListener('change', (state) => {
264
- if (state === 'active') refresh();
278
+ if (state === 'active') refreshAndSchedule();
279
+ else clearTimer();
265
280
  });
266
- return () => subscription.remove();
281
+ return () => {
282
+ clearTimer();
283
+ subscription.remove();
284
+ };
267
285
  }, []);
268
286
 
269
287
  return today;
@@ -0,0 +1,5 @@
1
+ export function millisecondsUntilNextLocalDay(now: Date): number {
2
+ const next = new Date(now);
3
+ next.setHours(24, 0, 0, 0);
4
+ return Math.max(next.getTime() - now.getTime(), 1);
5
+ }
@@ -112,6 +112,12 @@ import {
112
112
  type Plot as PlotBox,
113
113
  } from '../../utils/chart';
114
114
  import { cn } from '../../utils/cn';
115
+ import {
116
+ registerPlotSeries,
117
+ unregisterPlotSeries,
118
+ visiblePlotSeries,
119
+ type PlotSeriesRegistration,
120
+ } from './plot-series-registry';
115
121
 
116
122
  /*
117
123
  * The scale functions, re-exported so a mark written outside this file can sit
@@ -214,7 +220,7 @@ export interface PlotGeometry {
214
220
  /** Every mark that registered itself, as `[dataKey, colour]`. */
215
221
  series: [string, string][];
216
222
  registerSeries: (key: string, color: string) => void;
217
- unregisterSeries: (key: string) => void;
223
+ unregisterSeries: (key: string, color?: string) => void;
218
224
  /** Row under the cursor, or `-1`. On the UI thread. */
219
225
  activeIndex: SharedValue<number>;
220
226
  /** The same index on the JS thread, for anything that has to re-render. */
@@ -340,7 +346,7 @@ const PlotRoot = forwardRef<PlotHandle, PlotProps>(function PlotRoot(
340
346
  ref
341
347
  ) {
342
348
  const [size, setSize] = useState({ width: 0, height: 0 });
343
- const [series, setSeries] = useState<[string, string][]>([]);
349
+ const [registrations, setRegistrations] = useState<PlotSeriesRegistration[]>([]);
344
350
  const [activeIndexJS, setActiveIndexJS] = useState(-1);
345
351
  const clipId = `panelui-plot-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
346
352
 
@@ -352,19 +358,18 @@ const PlotRoot = forwardRef<PlotHandle, PlotProps>(function PlotRoot(
352
358
 
353
359
  const registerSeries = useMemo(
354
360
  () => (key: string, color: string) =>
355
- setSeries((current) => {
356
- const existing = current.find(([k]) => k === key);
357
- if (existing?.[1] === color) return current;
358
- return [...current.filter(([k]) => k !== key), [key, color]];
359
- }),
361
+ setRegistrations((current) => registerPlotSeries(current, key, color)),
360
362
  []
361
363
  );
362
364
 
363
365
  const unregisterSeries = useMemo(
364
- () => (key: string) => setSeries((current) => current.filter(([k]) => k !== key)),
366
+ () => (key: string, color?: string) =>
367
+ setRegistrations((current) => unregisterPlotSeries(current, key, color)),
365
368
  []
366
369
  );
367
370
 
371
+ const series = useMemo(() => visiblePlotSeries(registrations), [registrations]);
372
+
368
373
  /*
369
374
  * What the children need decided before anything is laid out.
370
375
  *
@@ -404,7 +409,7 @@ const PlotRoot = forwardRef<PlotHandle, PlotProps>(function PlotRoot(
404
409
  * magnitude apart look alike, which is the chart lying rather than the chart
405
410
  * being convenient.
406
411
  */
407
- const seriesKeys = series.map(([key]) => key).join('|');
412
+ const seriesKeys = useMemo(() => series.map(([key]) => key), [series]);
408
413
  const extent = useMemo<[number, number]>(() => {
409
414
  const lowPin = yDomain?.[0];
410
415
  const highPin = yDomain?.[1];
@@ -412,11 +417,10 @@ const PlotRoot = forwardRef<PlotHandle, PlotProps>(function PlotRoot(
412
417
  return [lowPin, highPin];
413
418
  }
414
419
 
415
- const keys = seriesKeys ? seriesKeys.split('|') : [];
416
420
  let min = Infinity;
417
421
  let max = -Infinity;
418
422
  for (const row of data) {
419
- for (const key of keys) {
423
+ for (const key of seriesKeys) {
420
424
  const value = row[key];
421
425
  if (typeof value !== 'number' || Number.isNaN(value)) continue;
422
426
  if (value < min) min = value;
@@ -692,7 +696,7 @@ function useMark(dataKey: string, color: string | undefined, colorIndex: number)
692
696
 
693
697
  useEffect(() => {
694
698
  registerSeries(dataKey, resolved);
695
- return () => unregisterSeries(dataKey);
699
+ return () => unregisterSeries(dataKey, resolved);
696
700
  }, [dataKey, resolved, registerSeries, unregisterSeries]);
697
701
 
698
702
  return resolved;
@@ -0,0 +1,41 @@
1
+ export interface PlotSeriesRegistration {
2
+ key: string;
3
+ colors: readonly string[];
4
+ }
5
+
6
+ export function registerPlotSeries(
7
+ current: readonly PlotSeriesRegistration[],
8
+ key: string,
9
+ color: string
10
+ ): PlotSeriesRegistration[] {
11
+ const existing = current.find((entry) => entry.key === key);
12
+ if (!existing) return [...current, { key, colors: [color] }];
13
+
14
+ return current.map((entry) =>
15
+ entry === existing ? { ...entry, colors: [...entry.colors, color] } : entry
16
+ );
17
+ }
18
+
19
+ export function unregisterPlotSeries(
20
+ current: readonly PlotSeriesRegistration[],
21
+ key: string,
22
+ color?: string
23
+ ): PlotSeriesRegistration[] {
24
+ const existing = current.find((entry) => entry.key === key);
25
+ if (!existing) return current as PlotSeriesRegistration[];
26
+
27
+ const index =
28
+ color === undefined ? existing.colors.length - 1 : existing.colors.lastIndexOf(color);
29
+ if (index < 0) return current as PlotSeriesRegistration[];
30
+
31
+ const colors = existing.colors.filter((_, candidate) => candidate !== index);
32
+ if (colors.length === 0) return current.filter((entry) => entry !== existing);
33
+
34
+ return current.map((entry) => (entry === existing ? { ...entry, colors } : entry));
35
+ }
36
+
37
+ export function visiblePlotSeries(
38
+ registrations: readonly PlotSeriesRegistration[]
39
+ ): [string, string][] {
40
+ return registrations.map(({ key, colors }) => [key, colors[colors.length - 1]!]);
41
+ }
@@ -36,6 +36,7 @@ import Animated, {
36
36
  import { tv, type VariantProps } from 'tailwind-variants';
37
37
  import { cn } from '../../utils/cn';
38
38
  import { Text, textChildren } from '../../primitives/text';
39
+ import { requestRadioValueChange } from './radio-group-value';
39
40
 
40
41
  type RadioVariant = 'dot' | 'card';
41
42
  type RadioOrientation = 'vertical' | 'horizontal';
@@ -247,7 +248,9 @@ const RadioGroupItem = forwardRef<View, RadioGroupItemProps>(
247
248
  accessibilityState={{ selected, disabled: !!disabled }}
248
249
  accessibilityLabel={label}
249
250
  disabled={disabled}
250
- onPress={() => context.onValueChange(value)}
251
+ onPress={() =>
252
+ requestRadioValueChange(context.value, value, context.onValueChange)
253
+ }
251
254
  className={slots.row({ className })}
252
255
  >
253
256
  {body}
@@ -0,0 +1,9 @@
1
+ export function requestRadioValueChange(
2
+ currentValue: string | undefined,
3
+ nextValue: string,
4
+ onValueChange: (value: string) => void
5
+ ) {
6
+ if (currentValue !== nextValue) {
7
+ onValueChange(nextValue);
8
+ }
9
+ }
@@ -43,6 +43,11 @@ import { StarIcon } from '../../icons';
43
43
  import { Text } from '../../primitives/text';
44
44
  import { cn } from '../../utils/cn';
45
45
  import { selectionTick } from '../../utils/haptics';
46
+ import {
47
+ normalizeRatingMax,
48
+ normalizeRatingPrecision,
49
+ normalizeRatingValue,
50
+ } from './rating-inputs';
46
51
 
47
52
  /** Springs the fill onto its resting value after a tap or the end of a drag. */
48
53
  const SPRING = { damping: 18, stiffness: 220, mass: 0.6 } as const;
@@ -197,8 +202,8 @@ export const Rating = forwardRef<View, RatingProps>(
197
202
  rowClassName,
198
203
  value: valueProp,
199
204
  defaultValue = 0,
200
- max = 5,
201
- precision = 1,
205
+ max: maxProp = 5,
206
+ precision: precisionProp = 1,
202
207
  onValueChange,
203
208
  onValueCommit,
204
209
  readOnly = false,
@@ -213,9 +218,11 @@ export const Rating = forwardRef<View, RatingProps>(
213
218
  },
214
219
  ref
215
220
  ) => {
221
+ const max = normalizeRatingMax(maxProp);
222
+ const precision = normalizeRatingPrecision(precisionProp);
216
223
  const isControlled = valueProp !== undefined;
217
- const [internal, setInternal] = useState(defaultValue);
218
- const value = clampJS(isControlled ? valueProp! : internal, 0, max);
224
+ const [internal, setInternal] = useState(() => normalizeRatingValue(defaultValue, max));
225
+ const value = normalizeRatingValue(isControlled ? valueProp : internal, max);
219
226
 
220
227
  const interactive = !readOnly && !disabled;
221
228
  const slots = ratingVariants({ size, disabled });
@@ -0,0 +1,19 @@
1
+ const DEFAULT_MAX = 5;
2
+ const DEFAULT_PRECISION = 1;
3
+
4
+ export function normalizeRatingMax(max: number | undefined): number {
5
+ return typeof max === 'number' && Number.isFinite(max) && max >= 1
6
+ ? Math.floor(max)
7
+ : DEFAULT_MAX;
8
+ }
9
+
10
+ export function normalizeRatingPrecision(precision: number | undefined): number {
11
+ return typeof precision === 'number' && Number.isFinite(precision) && precision > 0
12
+ ? precision
13
+ : DEFAULT_PRECISION;
14
+ }
15
+
16
+ export function normalizeRatingValue(value: number | undefined, max: number): number {
17
+ if (typeof value !== 'number' || !Number.isFinite(value)) return 0;
18
+ return Math.min(Math.max(value, 0), max);
19
+ }
@@ -70,6 +70,12 @@ import { AnimatedPressable } from '../../primitives/animated-pressable';
70
70
  import { Text } from '../../primitives/text';
71
71
  import { Input, type InputProps } from '../input';
72
72
  import { Spinner } from '../spinner';
73
+ import {
74
+ cancelSearchBarDebounce,
75
+ flushSearchBarDebounce,
76
+ scheduleSearchBarDebounce,
77
+ type SearchBarDebounceTimer,
78
+ } from './search-bar-debounce';
73
79
 
74
80
  /** Matches the field's own focus crossfade, so the row settles as one thing. */
75
81
  const CANCEL_DURATION = 180;
@@ -251,6 +257,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(
251
257
  * render and push the pause out forever.
252
258
  */
253
259
  const debouncedRef = useRef(onDebouncedChange);
260
+ const debounceTimerRef = useRef<SearchBarDebounceTimer['current']>(null);
254
261
  useEffect(() => {
255
262
  debouncedRef.current = onDebouncedChange;
256
263
  });
@@ -263,12 +270,10 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(
263
270
  settled.current = true;
264
271
  return;
265
272
  }
266
- if (debounce <= 0) {
267
- debouncedRef.current?.(text);
268
- return;
269
- }
270
- const timer = setTimeout(() => debouncedRef.current?.(text), debounce);
271
- return () => clearTimeout(timer);
273
+ scheduleSearchBarDebounce(debounceTimerRef, debouncedRef.current, text, debounce);
274
+ return () => {
275
+ cancelSearchBarDebounce(debounceTimerRef);
276
+ };
272
277
  }, [text, debounce]);
273
278
 
274
279
  const handleFocus = useCallback<NonNullable<InputProps['onFocus']>>(
@@ -291,7 +296,7 @@ export const SearchBar = forwardRef<TextInput, SearchBarProps>(
291
296
  (event: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
292
297
  // A return key is somebody saying they are done waiting, so the
293
298
  // pending pause is spent rather than waited out.
294
- debouncedRef.current?.(text);
299
+ flushSearchBarDebounce(debounceTimerRef, debouncedRef.current, text);
295
300
  onSubmit?.(text);
296
301
  onSubmitEditing?.(event);
297
302
  },
@@ -0,0 +1,35 @@
1
+ export interface SearchBarDebounceTimer {
2
+ current: ReturnType<typeof setTimeout> | null;
3
+ }
4
+
5
+ export function cancelSearchBarDebounce(timer: SearchBarDebounceTimer): void {
6
+ if (timer.current === null) return;
7
+ clearTimeout(timer.current);
8
+ timer.current = null;
9
+ }
10
+
11
+ export function scheduleSearchBarDebounce(
12
+ timer: SearchBarDebounceTimer,
13
+ callback: ((value: string) => void) | undefined,
14
+ value: string,
15
+ delay: number
16
+ ): void {
17
+ cancelSearchBarDebounce(timer);
18
+ if (!Number.isFinite(delay) || delay <= 0) {
19
+ callback?.(value);
20
+ return;
21
+ }
22
+ timer.current = setTimeout(() => {
23
+ timer.current = null;
24
+ callback?.(value);
25
+ }, delay);
26
+ }
27
+
28
+ export function flushSearchBarDebounce(
29
+ timer: SearchBarDebounceTimer,
30
+ callback: ((value: string) => void) | undefined,
31
+ value: string
32
+ ): void {
33
+ cancelSearchBarDebounce(timer);
34
+ callback?.(value);
35
+ }
@@ -70,6 +70,7 @@ import { useBackHandler } from '../../hooks/use-back-handler';
70
70
  import { cn } from '../../utils/cn';
71
71
  import { BottomSheet } from '../bottom-sheet';
72
72
  import { InputGroup } from '../input-group';
73
+ import { nativeSelectSupportsOptions } from './native-select-contract';
73
74
 
74
75
  const selectVariants = tv({
75
76
  slots: {
@@ -339,7 +340,6 @@ function SelectRoot({
339
340
  const triggerRef = useRef<View>(null);
340
341
  const chevron = useSharedValue(0);
341
342
  const { height: screenHeight } = useWindowDimensions();
342
- const nativeUI = native ? getNativeUI() : null;
343
343
 
344
344
  const options = useMemo(() => {
345
345
  const collected: SelectItemProps[] = [];
@@ -347,6 +347,15 @@ function SelectRoot({
347
347
  return collected;
348
348
  }, [children]);
349
349
 
350
+ /*
351
+ * @expo/ui's portable Picker only has a control-wide `enabled` flag. Its
352
+ * items accept label and value, but no disabled state, so handing it a
353
+ * disabled Select.Item would make that option selectable again. Preserve
354
+ * the public item contract by using the styled Select for that list.
355
+ */
356
+ const nativeUI =
357
+ native && nativeSelectSupportsOptions(options) ? getNativeUI() : null;
358
+
350
359
  const selectedLabel = useMemo(
351
360
  () => options.find((option) => option.value === value)?.label,
352
361
  [options, value]
@@ -0,0 +1,12 @@
1
+ interface NativeSelectOption {
2
+ disabled?: boolean;
3
+ }
4
+
5
+ /**
6
+ * The portable @expo/ui picker can disable the whole control, but not one
7
+ * option. Keep the styled Select when an item needs that distinction rather
8
+ * than silently turning a disabled choice back into a selectable one.
9
+ */
10
+ export function nativeSelectSupportsOptions(options: NativeSelectOption[]): boolean {
11
+ return options.every((option) => !option.disabled);
12
+ }