panelui-native 0.68.0 → 0.73.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 (206) hide show
  1. package/README.md +13 -2
  2. package/lib/module/components/accordion/index.js +38 -13
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/area-chart/index.js +1 -0
  5. package/lib/module/components/area-chart/index.js.map +1 -1
  6. package/lib/module/components/breadcrumb/index.js +2 -1
  7. package/lib/module/components/breadcrumb/index.js.map +1 -1
  8. package/lib/module/components/button/index.js +21 -12
  9. package/lib/module/components/button/index.js.map +1 -1
  10. package/lib/module/components/carousel/index.js +1 -1
  11. package/lib/module/components/carousel/index.js.map +1 -1
  12. package/lib/module/components/chip/index.js +2 -2
  13. package/lib/module/components/chip/index.js.map +1 -1
  14. package/lib/module/components/code-block/index.js.map +1 -1
  15. package/lib/module/components/collapsible/index.js +284 -0
  16. package/lib/module/components/collapsible/index.js.map +1 -0
  17. package/lib/module/components/color-picker/index.js.map +1 -1
  18. package/lib/module/components/fab/index.js +7 -6
  19. package/lib/module/components/fab/index.js.map +1 -1
  20. package/lib/module/components/flow/index.js +67 -4
  21. package/lib/module/components/flow/index.js.map +1 -1
  22. package/lib/module/components/frame/index.js +2 -2
  23. package/lib/module/components/frame/index.js.map +1 -1
  24. package/lib/module/components/grid-item/index.js +2 -2
  25. package/lib/module/components/grid-item/index.js.map +1 -1
  26. package/lib/module/components/heatmap-chart/index.js +1 -0
  27. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  28. package/lib/module/components/hex-chart/index.js +1 -0
  29. package/lib/module/components/hex-chart/index.js.map +1 -1
  30. package/lib/module/components/input-group/index.js +7 -2
  31. package/lib/module/components/input-group/index.js.map +1 -1
  32. package/lib/module/components/item/index.js +2 -2
  33. package/lib/module/components/item/index.js.map +1 -1
  34. package/lib/module/components/line-chart/index.js +1 -0
  35. package/lib/module/components/line-chart/index.js.map +1 -1
  36. package/lib/module/components/live-line-chart/index.js +103 -9
  37. package/lib/module/components/live-line-chart/index.js.map +1 -1
  38. package/lib/module/components/live-line-chart/live-line-accessibility.js +47 -0
  39. package/lib/module/components/live-line-chart/live-line-accessibility.js.map +1 -0
  40. package/lib/module/components/live-line-chart/live-line-lifecycle.js +33 -0
  41. package/lib/module/components/live-line-chart/live-line-lifecycle.js.map +1 -0
  42. package/lib/module/components/marker/index.js +2 -2
  43. package/lib/module/components/marker/index.js.map +1 -1
  44. package/lib/module/components/marquee/index.js +191 -0
  45. package/lib/module/components/marquee/index.js.map +1 -0
  46. package/lib/module/components/marquee/marquee-math.js +39 -0
  47. package/lib/module/components/marquee/marquee-math.js.map +1 -0
  48. package/lib/module/components/menu/index.js +11 -3
  49. package/lib/module/components/menu/index.js.map +1 -1
  50. package/lib/module/components/meter/index.js +38 -20
  51. package/lib/module/components/meter/index.js.map +1 -1
  52. package/lib/module/components/meter/meter-scale.js +62 -8
  53. package/lib/module/components/meter/meter-scale.js.map +1 -1
  54. package/lib/module/components/pagination/index.js +27 -11
  55. package/lib/module/components/pagination/index.js.map +1 -1
  56. package/lib/module/components/panelside/index.js +7 -2
  57. package/lib/module/components/panelside/index.js.map +1 -1
  58. package/lib/module/components/planner/index.js +159 -64
  59. package/lib/module/components/planner/index.js.map +1 -1
  60. package/lib/module/components/planner/planner-announcement.js +30 -0
  61. package/lib/module/components/planner/planner-announcement.js.map +1 -0
  62. package/lib/module/components/planner/planner-entries.js +15 -7
  63. package/lib/module/components/planner/planner-entries.js.map +1 -1
  64. package/lib/module/components/planner/planner-grid-navigation.js +33 -0
  65. package/lib/module/components/planner/planner-grid-navigation.js.map +1 -0
  66. package/lib/module/components/planner/planner-lifecycle.js +45 -0
  67. package/lib/module/components/planner/planner-lifecycle.js.map +1 -0
  68. package/lib/module/components/plot/index.js +105 -42
  69. package/lib/module/components/plot/index.js.map +1 -1
  70. package/lib/module/components/popover/index.js +2 -2
  71. package/lib/module/components/popover/index.js.map +1 -1
  72. package/lib/module/components/post/index.js +4 -4
  73. package/lib/module/components/post/index.js.map +1 -1
  74. package/lib/module/components/search-bar/index.js +331 -0
  75. package/lib/module/components/search-bar/index.js.map +1 -0
  76. package/lib/module/components/table/index.js +3 -4
  77. package/lib/module/components/table/index.js.map +1 -1
  78. package/lib/module/components/timeline/index.js +288 -39
  79. package/lib/module/components/timeline/index.js.map +1 -1
  80. package/lib/module/components/toggle-button/index.js +1 -1
  81. package/lib/module/components/toggle-button/index.js.map +1 -1
  82. package/lib/module/components/tree/index.js +13 -8
  83. package/lib/module/components/tree/index.js.map +1 -1
  84. package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
  85. package/lib/module/index.js +3 -0
  86. package/lib/module/index.js.map +1 -1
  87. package/lib/module/primitives/focus-restoration-store.js +52 -0
  88. package/lib/module/primitives/focus-restoration-store.js.map +1 -0
  89. package/lib/module/primitives/portal.js +36 -2
  90. package/lib/module/primitives/portal.js.map +1 -1
  91. package/lib/module/utils/chart.js +50 -7
  92. package/lib/module/utils/chart.js.map +1 -1
  93. package/lib/typescript/src/components/accordion/index.d.ts +6 -2
  94. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  95. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  96. package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -1
  97. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  98. package/lib/typescript/src/components/code-block/index.d.ts +3 -3
  99. package/lib/typescript/src/components/code-block/index.d.ts.map +1 -1
  100. package/lib/typescript/src/components/collapsible/index.d.ts +76 -0
  101. package/lib/typescript/src/components/collapsible/index.d.ts.map +1 -0
  102. package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -1
  103. package/lib/typescript/src/components/fab/index.d.ts +4 -2
  104. package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
  105. package/lib/typescript/src/components/flow/index.d.ts +7 -1
  106. package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
  107. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  108. package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
  109. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
  110. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  111. package/lib/typescript/src/components/live-line-chart/index.d.ts +21 -7
  112. package/lib/typescript/src/components/live-line-chart/index.d.ts.map +1 -1
  113. package/lib/typescript/src/components/live-line-chart/live-line-accessibility.d.ts +22 -0
  114. package/lib/typescript/src/components/live-line-chart/live-line-accessibility.d.ts.map +1 -0
  115. package/lib/typescript/src/components/live-line-chart/live-line-lifecycle.d.ts +23 -0
  116. package/lib/typescript/src/components/live-line-chart/live-line-lifecycle.d.ts.map +1 -0
  117. package/lib/typescript/src/components/marquee/index.d.ts +73 -0
  118. package/lib/typescript/src/components/marquee/index.d.ts.map +1 -0
  119. package/lib/typescript/src/components/marquee/marquee-math.d.ts +21 -0
  120. package/lib/typescript/src/components/marquee/marquee-math.d.ts.map +1 -0
  121. package/lib/typescript/src/components/menu/index.d.ts +1 -1
  122. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  123. package/lib/typescript/src/components/meter/index.d.ts +7 -4
  124. package/lib/typescript/src/components/meter/index.d.ts.map +1 -1
  125. package/lib/typescript/src/components/meter/meter-scale.d.ts +39 -0
  126. package/lib/typescript/src/components/meter/meter-scale.d.ts.map +1 -1
  127. package/lib/typescript/src/components/pagination/index.d.ts.map +1 -1
  128. package/lib/typescript/src/components/panelside/index.d.ts +1 -1
  129. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  130. package/lib/typescript/src/components/plan/index.d.ts +2 -2
  131. package/lib/typescript/src/components/planner/index.d.ts.map +1 -1
  132. package/lib/typescript/src/components/planner/planner-announcement.d.ts +15 -0
  133. package/lib/typescript/src/components/planner/planner-announcement.d.ts.map +1 -0
  134. package/lib/typescript/src/components/planner/planner-entries.d.ts +1 -1
  135. package/lib/typescript/src/components/planner/planner-entries.d.ts.map +1 -1
  136. package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts +4 -0
  137. package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts.map +1 -0
  138. package/lib/typescript/src/components/planner/planner-lifecycle.d.ts +20 -0
  139. package/lib/typescript/src/components/planner/planner-lifecycle.d.ts.map +1 -0
  140. package/lib/typescript/src/components/plot/index.d.ts +59 -5
  141. package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
  142. package/lib/typescript/src/components/search-bar/index.d.ts +216 -0
  143. package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -0
  144. package/lib/typescript/src/components/timeline/index.d.ts +35 -2
  145. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  146. package/lib/typescript/src/components/tree/index.d.ts +3 -4
  147. package/lib/typescript/src/components/tree/index.d.ts.map +1 -1
  148. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +2 -2
  149. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
  150. package/lib/typescript/src/index.d.ts +4 -1
  151. package/lib/typescript/src/index.d.ts.map +1 -1
  152. package/lib/typescript/src/primitives/focus-restoration-store.d.ts +24 -0
  153. package/lib/typescript/src/primitives/focus-restoration-store.d.ts.map +1 -0
  154. package/lib/typescript/src/primitives/portal.d.ts +4 -0
  155. package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
  156. package/lib/typescript/src/utils/chart.d.ts +28 -3
  157. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  158. package/package.json +36 -3
  159. package/src/components/accordion/index.tsx +48 -15
  160. package/src/components/area-chart/index.tsx +8 -0
  161. package/src/components/breadcrumb/index.tsx +2 -1
  162. package/src/components/button/index.tsx +14 -6
  163. package/src/components/carousel/index.tsx +1 -1
  164. package/src/components/chip/index.tsx +2 -2
  165. package/src/components/code-block/index.tsx +3 -3
  166. package/src/components/collapsible/index.tsx +287 -0
  167. package/src/components/color-picker/index.tsx +3 -2
  168. package/src/components/fab/index.tsx +21 -11
  169. package/src/components/flow/index.tsx +94 -15
  170. package/src/components/frame/index.tsx +2 -2
  171. package/src/components/grid-item/index.tsx +2 -2
  172. package/src/components/heatmap-chart/index.tsx +7 -1
  173. package/src/components/hex-chart/index.tsx +7 -1
  174. package/src/components/input-group/index.tsx +7 -2
  175. package/src/components/item/index.tsx +2 -2
  176. package/src/components/line-chart/index.tsx +9 -0
  177. package/src/components/live-line-chart/index.tsx +130 -13
  178. package/src/components/live-line-chart/live-line-accessibility.ts +61 -0
  179. package/src/components/live-line-chart/live-line-lifecycle.ts +63 -0
  180. package/src/components/marker/index.tsx +2 -2
  181. package/src/components/marquee/index.tsx +234 -0
  182. package/src/components/marquee/marquee-math.ts +37 -0
  183. package/src/components/menu/index.tsx +11 -3
  184. package/src/components/meter/index.tsx +49 -26
  185. package/src/components/meter/meter-scale.ts +84 -8
  186. package/src/components/pagination/index.tsx +26 -13
  187. package/src/components/panelside/index.tsx +7 -2
  188. package/src/components/plan/index.tsx +2 -2
  189. package/src/components/planner/index.tsx +202 -76
  190. package/src/components/planner/planner-announcement.ts +37 -0
  191. package/src/components/planner/planner-entries.ts +16 -7
  192. package/src/components/planner/planner-grid-navigation.ts +45 -0
  193. package/src/components/planner/planner-lifecycle.ts +76 -0
  194. package/src/components/plot/index.tsx +183 -39
  195. package/src/components/popover/index.tsx +3 -3
  196. package/src/components/post/index.tsx +4 -4
  197. package/src/components/search-bar/index.tsx +436 -0
  198. package/src/components/table/index.tsx +2 -2
  199. package/src/components/timeline/index.tsx +335 -39
  200. package/src/components/toggle-button/index.tsx +1 -1
  201. package/src/components/tree/index.tsx +17 -12
  202. package/src/hooks/use-keyboard-avoidance.ts +7 -2
  203. package/src/index.ts +15 -0
  204. package/src/primitives/focus-restoration-store.ts +70 -0
  205. package/src/primitives/portal.tsx +34 -2
  206. package/src/utils/chart.ts +66 -7
@@ -451,6 +451,8 @@ export function MenuItem({
451
451
  closeOnSelect = true,
452
452
  onSelect,
453
453
  onPress,
454
+ onPressIn,
455
+ onPressOut,
454
456
  ...props
455
457
  }: MenuItemProps) {
456
458
  const { close, haptics } = useMenu('Menu.Item');
@@ -467,15 +469,21 @@ export function MenuItem({
467
469
 
468
470
  return (
469
471
  <AnimatedPressable
472
+ {...props}
470
473
  accessibilityRole="menuitem"
471
474
  accessibilityState={{ disabled }}
472
475
  disabled={disabled}
473
476
  onPress={handlePress}
474
- onPressIn={press.onPressIn}
475
- onPressOut={press.onPressOut}
477
+ onPressIn={(event) => {
478
+ onPressIn?.(event);
479
+ press.onPressIn();
480
+ }}
481
+ onPressOut={(event) => {
482
+ onPressOut?.(event);
483
+ press.onPressOut();
484
+ }}
476
485
  style={press.rowStyle}
477
486
  className={cn(slots.item(), 'overflow-hidden', className)}
478
- {...props}
479
487
  >
480
488
  <Animated.View
481
489
  pointerEvents="none"
@@ -11,16 +11,21 @@ import Animated, {
11
11
  import { tv, type VariantProps } from 'tailwind-variants';
12
12
  import { Text } from '../../primitives/text';
13
13
  import {
14
- clamp,
15
14
  colorFor,
16
- fractionOf,
17
- formatValue,
18
15
  litSegments,
16
+ meterSemantics,
17
+ normalizeScale,
18
+ normalizeSegments,
19
19
  type MeterColor,
20
20
  type MeterThreshold,
21
21
  } from './meter-scale';
22
22
 
23
- const SPRING = { damping: 20, stiffness: 180, mass: 0.6 } as const;
23
+ const SPRING = {
24
+ damping: 20,
25
+ stiffness: 180,
26
+ mass: 0.6,
27
+ overshootClamping: true,
28
+ } as const;
24
29
  /** Milliseconds for a segment to light or go out. */
25
30
  const SEGMENT_DURATION = 180;
26
31
  /** How faint an unlit segment sits. Present, but plainly not counted. */
@@ -32,7 +37,7 @@ const meterVariants = tv({
32
37
  header: 'flex-row items-center justify-between gap-2',
33
38
  track: 'w-full overflow-hidden rounded-full',
34
39
  indicator: 'h-full rounded-full',
35
- segments: 'w-full flex-row',
40
+ segments: 'w-full flex-row overflow-hidden',
36
41
  segment: 'flex-1 rounded-full',
37
42
  },
38
43
  variants: {
@@ -70,7 +75,7 @@ export interface MeterProps
70
75
  extends Omit<ViewProps, 'children'>,
71
76
  MeterVariantProps {
72
77
  className?: string;
73
- /** The measurement, somewhere between `minValue` and `maxValue`. */
78
+ /** The measurement. Values outside the scale are clamped to its ends. */
74
79
  value: number;
75
80
  /**
76
81
  * The bottom of the scale — the value at which the bar reads as empty.
@@ -111,7 +116,8 @@ export interface MeterProps
111
116
  /**
112
117
  * Points on the scale where the colour changes, each `{ from, color }`. The
113
118
  * highest one the reading has reached wins, so the order you list them in
114
- * does not matter; below all of them the `color` prop applies.
119
+ * does not matter; below all of them the `color` prop applies. Non-finite
120
+ * `from` values are ignored.
115
121
  *
116
122
  * This is the difference between a meter and a bar: the colour is a
117
123
  * judgement about the reading. Which direction is bad is yours to say —
@@ -125,8 +131,10 @@ export interface MeterProps
125
131
  * say "three out of four" where a bar says "about seventy percent", and a
126
132
  * password is not seventy percent strong.
127
133
  *
128
- * Any value above the floor lights at least one block, so a reading that
129
- * is not empty never looks it.
134
+ * Fractional counts round down and counts above 100 clamp to 100. Invalid,
135
+ * non-positive, or sub-one counts use the continuous bar. Any value above
136
+ * the floor lights at least one block, so a reading that is not empty never
137
+ * looks it.
130
138
  */
131
139
  segments?: number;
132
140
  /** Extra classes for the fill, or for a lit segment. */
@@ -207,8 +215,9 @@ export const Meter = forwardRef<View, MeterProps>(
207
215
  },
208
216
  ref
209
217
  ) => {
210
- const held = clamp(value, minValue, maxValue);
211
- const fraction = fractionOf(value, minValue, maxValue);
218
+ const scale = normalizeScale(value, minValue, maxValue);
219
+ const { value: held, fraction } = scale;
220
+ const segmentCount = normalizeSegments(segments);
212
221
  const resolvedColor = colorFor(held, color ?? 'primary', thresholds);
213
222
  const slots = meterVariants({ color: resolvedColor, size });
214
223
  const reducedMotion = useReducedMotion();
@@ -218,7 +227,7 @@ export const Meter = forwardRef<View, MeterProps>(
218
227
  // Any reading above the floor lights a block, so "a little" never looks
219
228
  // like "none". Rounding down would leave the first quarter of a
220
229
  // four-block meter dark, which is the reading it is least able to afford.
221
- const litCount = segments ? litSegments(fraction, segments) : 0;
230
+ const litCount = litSegments(fraction, segmentCount);
222
231
  const lit = useSharedValue(litCount);
223
232
 
224
233
  // Reduce motion lands on the value instead of springing to it — the
@@ -232,39 +241,47 @@ export const Meter = forwardRef<View, MeterProps>(
232
241
  }, [lit, litCount]);
233
242
 
234
243
  const onLayout = (event: LayoutChangeEvent) => {
235
- trackWidth.value = event.nativeEvent.layout.width;
244
+ const width = event.nativeEvent.layout.width;
245
+ trackWidth.value = Number.isFinite(width) && width > 0 ? width : 0;
236
246
  };
237
247
 
238
248
  const fillStyle = useAnimatedStyle(() => ({
239
249
  width: trackWidth.value * progress.value,
240
250
  }));
241
251
 
242
- const spoken = formatValue(held, fraction, valueLabel, formatOptions);
252
+ const semantics = meterSemantics({
253
+ value: held,
254
+ fraction,
255
+ // The normalized ends, not the raw props: an invalid scale is repaired
256
+ // for the bar, and the spoken reading has to describe the same one.
257
+ minValue: scale.min,
258
+ maxValue: scale.max,
259
+ label,
260
+ accessibilityLabel,
261
+ valueLabel,
262
+ formatOptions,
263
+ });
264
+ const spoken = semantics.text;
243
265
  const showValue = showValueLabel;
244
266
  const hasHeader = label != null || showValue;
245
267
 
246
268
  const accessibility = {
247
269
  accessibilityRole: 'progressbar' as const,
248
- accessibilityLabel: accessibilityLabel ?? label,
249
- accessibilityValue: {
250
- min: minValue,
251
- max: maxValue,
252
- now: held,
253
- // Without this the scale is read as a bare number. `text` is what
254
- // carries the unit — the percent sign, the gigabytes, the word.
255
- text: spoken,
256
- },
270
+ accessibilityLabel: semantics.label,
271
+ // `text` carries the unit or word and intentionally overrides the
272
+ // platform's generic rendering of this numeric range.
273
+ accessibilityValue: semantics.value,
257
274
  };
258
275
 
259
276
  const bar =
260
- segments && segments > 0 ? (
277
+ segmentCount > 0 ? (
261
278
  <View
262
279
  ref={ref}
263
280
  {...accessibility}
264
281
  className={slots.segments({ className })}
265
282
  {...props}
266
283
  >
267
- {Array.from({ length: segments }, (_, index) => (
284
+ {Array.from({ length: segmentCount }, (_, index) => (
268
285
  <Segment
269
286
  key={index}
270
287
  index={index}
@@ -293,7 +310,13 @@ export const Meter = forwardRef<View, MeterProps>(
293
310
 
294
311
  return (
295
312
  <View className={slots.root()}>
296
- <View className={slots.header({ className: headerClassName })}>
313
+ <View
314
+ className={slots.header({ className: headerClassName })}
315
+ // This is the visual rendering of the name and value already owned
316
+ // by the bar below. Hiding the subtree keeps one concise focus stop.
317
+ accessibilityElementsHidden
318
+ importantForAccessibility="no-hide-descendants"
319
+ >
297
320
  {label != null ? (
298
321
  <Text size="sm" weight="medium" numberOfLines={1}>
299
322
  {label}
@@ -29,11 +29,55 @@ export interface MeterThreshold {
29
29
  color: MeterColor;
30
30
  }
31
31
 
32
+ /** A bounded block count keeps malformed input from allocating without limit. */
33
+ export const MAX_METER_SEGMENTS = 100;
34
+
35
+ export interface MeterScale {
36
+ min: number;
37
+ max: number;
38
+ value: number;
39
+ fraction: number;
40
+ }
41
+
42
+ /**
43
+ * Make a public scale safe for layout, animation and native accessibility.
44
+ * Invalid bounds fall back to their documented defaults; if those still do
45
+ * not produce a positive span, the scale honestly collapses at its floor.
46
+ */
47
+ export function normalizeScale(value: number, min: number, max: number): MeterScale {
48
+ const floor = Number.isFinite(min) ? min : 0;
49
+ const candidateMax = Number.isFinite(max) ? max : 100;
50
+ const ceiling = candidateMax > floor ? candidateMax : floor;
51
+ const held = Number.isNaN(value)
52
+ ? floor
53
+ : value === Number.POSITIVE_INFINITY
54
+ ? ceiling
55
+ : value === Number.NEGATIVE_INFINITY
56
+ ? floor
57
+ : Math.min(Math.max(value, floor), ceiling);
58
+ const magnitude = Math.max(Math.abs(floor), Math.abs(ceiling), 1);
59
+ const position =
60
+ ceiling > floor
61
+ ? (held / magnitude - floor / magnitude) /
62
+ (ceiling / magnitude - floor / magnitude)
63
+ : 0;
64
+ return {
65
+ min: floor,
66
+ max: ceiling,
67
+ value: held,
68
+ fraction: Math.min(Math.max(position, 0), 1),
69
+ };
70
+ }
71
+
72
+ /** Positive counts are whole and bounded; zero means the continuous meter. */
73
+ export function normalizeSegments(segments: number | undefined) {
74
+ if (segments == null || !Number.isFinite(segments) || !(segments > 0)) return 0;
75
+ return Math.min(Math.floor(segments), MAX_METER_SEGMENTS);
76
+ }
77
+
32
78
  /** `value` held inside the scale, so a stray number cannot escape the track. */
33
79
  export function clamp(value: number, min: number, max: number) {
34
- if (!(value > min)) return min;
35
- if (value > max) return max;
36
- return value;
80
+ return normalizeScale(value, min, max).value;
37
81
  }
38
82
 
39
83
  /**
@@ -41,9 +85,7 @@ export function clamp(value: number, min: number, max: number) {
41
85
  * meaningful position in it, so it reads as empty rather than dividing by zero.
42
86
  */
43
87
  export function fractionOf(value: number, min: number, max: number) {
44
- const span = max - min;
45
- if (!(span > 0)) return 0;
46
- return clamp((value - min) / span, 0, 1);
88
+ return normalizeScale(value, min, max).fraction;
47
89
  }
48
90
 
49
91
  /**
@@ -62,6 +104,7 @@ export function colorFor(
62
104
  if (!thresholds?.length) return base;
63
105
  let winner: MeterThreshold | undefined;
64
106
  for (const threshold of thresholds) {
107
+ if (!Number.isFinite(threshold.from)) continue;
65
108
  if (value < threshold.from) continue;
66
109
  if (!winner || threshold.from > winner.from) winner = threshold;
67
110
  }
@@ -77,8 +120,10 @@ export function colorFor(
77
120
  * wrong.
78
121
  */
79
122
  export function litSegments(fraction: number, segments: number) {
80
- if (!(segments > 0)) return 0;
81
- return Math.min(Math.ceil(fraction * segments), segments);
123
+ const count = normalizeSegments(segments);
124
+ if (count === 0 || Number.isNaN(fraction) || fraction <= 0) return 0;
125
+ if (!Number.isFinite(fraction) || fraction >= 1) return count;
126
+ return Math.min(Math.ceil(fraction * count), count);
82
127
  }
83
128
 
84
129
  /**
@@ -107,3 +152,34 @@ export function formatValue(
107
152
  }
108
153
  return `${Math.round(fraction * 100)}%`;
109
154
  }
155
+
156
+ /**
157
+ * The meter's single spoken contract. Visual treatments such as segments,
158
+ * thresholds and motion do not belong here: they do not change the reading.
159
+ */
160
+ export function meterSemantics({
161
+ value,
162
+ fraction,
163
+ minValue,
164
+ maxValue,
165
+ label,
166
+ accessibilityLabel,
167
+ valueLabel,
168
+ formatOptions,
169
+ }: {
170
+ value: number;
171
+ fraction: number;
172
+ minValue: number;
173
+ maxValue: number;
174
+ label?: string;
175
+ accessibilityLabel?: string;
176
+ valueLabel?: string;
177
+ formatOptions?: Intl.NumberFormatOptions;
178
+ }) {
179
+ const text = formatValue(value, fraction, valueLabel, formatOptions);
180
+ return {
181
+ label: accessibilityLabel ?? label,
182
+ text,
183
+ value: { min: minValue, max: maxValue, now: value, text },
184
+ };
185
+ }
@@ -367,7 +367,7 @@ export interface PaginationItemProps
367
367
  * are on, so the current page is spoken as state rather than only painted.
368
368
  */
369
369
  const PaginationItem = forwardRef<View, PaginationItemProps>(
370
- ({ className, labelClassName, page, children, ...props }, ref) => {
370
+ ({ className, labelClassName, page, children, onPress, ...props }, ref) => {
371
371
  const { page: current, size, disabled, goTo } = usePagination('Pagination.Item');
372
372
  const isCurrent = page === current;
373
373
  const { item, itemLabel } = paginationVariants({ size, current: isCurrent, disabled });
@@ -375,14 +375,17 @@ const PaginationItem = forwardRef<View, PaginationItemProps>(
375
375
  return (
376
376
  <AnimatedPressable
377
377
  ref={ref}
378
+ {...props}
378
379
  accessibilityRole="button"
379
380
  accessibilityLabel={`Page ${page}`}
380
381
  accessibilityState={{ selected: isCurrent, disabled }}
381
382
  disabled={disabled}
382
383
  hitSlop={size === 'sm' ? SMALL_HIT_SLOP : undefined}
383
- onPress={() => goTo(page)}
384
+ onPress={(event) => {
385
+ onPress?.(event);
386
+ goTo(page);
387
+ }}
384
388
  className={item({ className })}
385
- {...props}
386
389
  >
387
390
  {textChildren(children ?? String(page), (text) => (
388
391
  <Text className={itemLabel({ className: labelClassName })}>{text}</Text>
@@ -437,23 +440,29 @@ function PaginationArrow({
437
440
  word,
438
441
  className,
439
442
  label,
443
+ disabled,
444
+ onPress,
440
445
  ...props
441
446
  }: PaginationArrowProps & { step: -1 | 1; part: string; word: string }) {
442
447
  const { Glyph, size, spent, color, press } = useArrow(step, part);
443
- const { item, itemLabel } = paginationVariants({ size, disabled: spent });
448
+ const isDisabled = Boolean(spent || disabled);
449
+ const { item, itemLabel } = paginationVariants({ size, disabled: isDisabled });
444
450
 
445
451
  return (
446
452
  <AnimatedPressable
453
+ {...props}
447
454
  accessibilityRole="button"
448
455
  accessibilityLabel={word}
449
- accessibilityState={{ disabled: spent }}
450
- disabled={spent}
456
+ accessibilityState={{ disabled: isDisabled }}
457
+ disabled={isDisabled}
451
458
  hitSlop={size === 'sm' ? SMALL_HIT_SLOP : undefined}
452
- onPress={press}
459
+ onPress={(event) => {
460
+ onPress?.(event);
461
+ press();
462
+ }}
453
463
  className={item({
454
464
  className: cn('flex-row gap-1', label && (size === 'sm' ? 'px-2' : 'px-3'), className),
455
465
  })}
456
- {...props}
457
466
  >
458
467
  {step === -1 ? <Glyph size={iconSize[size]} color={color} /> : null}
459
468
  {label ? <Text className={itemLabel()}>{word}</Text> : null}
@@ -489,24 +498,28 @@ export interface PaginationEllipsisProps
489
498
  * that always went forwards would strand anyone reading the row right to left.
490
499
  */
491
500
  const PaginationEllipsis = forwardRef<View, PaginationEllipsisProps>(
492
- ({ className, direction = 1, jump = 5, ...props }, ref) => {
501
+ ({ className, direction = 1, jump = 5, disabled: disabledProp, onPress, ...props }, ref) => {
493
502
  const { page, size, disabled, goTo } = usePagination('Pagination.Ellipsis');
494
503
  const color = useCSSVariable('--color-muted-foreground');
495
504
  const { ellipsis } = paginationVariants({ size });
505
+ const isDisabled = Boolean(disabled || disabledProp);
496
506
 
497
507
  return (
498
508
  <AnimatedPressable
499
509
  ref={ref}
510
+ {...props}
500
511
  accessibilityRole="button"
501
512
  accessibilityLabel={
502
513
  direction === -1 ? `Back ${jump} pages` : `Forward ${jump} pages`
503
514
  }
504
- accessibilityState={{ disabled }}
505
- disabled={disabled}
515
+ accessibilityState={{ disabled: isDisabled }}
516
+ disabled={isDisabled}
506
517
  hitSlop={size === 'sm' ? SMALL_HIT_SLOP : undefined}
507
- onPress={() => goTo(page + direction * jump)}
518
+ onPress={(event) => {
519
+ onPress?.(event);
520
+ goTo(page + direction * jump);
521
+ }}
508
522
  className={ellipsis({ className })}
509
- {...props}
510
523
  >
511
524
  <EllipsisIcon
512
525
  size={iconSize[size]}
@@ -1412,6 +1412,7 @@ function PanelsideTrigger({
1412
1412
  className,
1413
1413
  label = 'Open navigation panel',
1414
1414
  children,
1415
+ onPress,
1415
1416
  ...props
1416
1417
  }: PanelsideTriggerProps) {
1417
1418
  const { toggle, docked } = usePanelsideContext('Panelside.Trigger');
@@ -1426,6 +1427,7 @@ function PanelsideTrigger({
1426
1427
  return cloneElement(children, {
1427
1428
  onPress: (...args: unknown[]) => {
1428
1429
  children.props.onPress?.(...args);
1430
+ onPress?.(...(args as Parameters<NonNullable<PressableProps['onPress']>>));
1429
1431
  toggle();
1430
1432
  },
1431
1433
  });
@@ -1433,11 +1435,14 @@ function PanelsideTrigger({
1433
1435
 
1434
1436
  return (
1435
1437
  <AnimatedPressable
1436
- onPress={toggle}
1438
+ {...props}
1439
+ onPress={(event) => {
1440
+ onPress?.(event);
1441
+ toggle();
1442
+ }}
1437
1443
  className={cn('h-10 w-10 items-center justify-center rounded-full', className)}
1438
1444
  accessibilityRole="button"
1439
1445
  accessibilityLabel={label}
1440
- {...props}
1441
1446
  >
1442
1447
  <MenuIcon size={20} color={color} />
1443
1448
  </AnimatedPressable>
@@ -285,7 +285,7 @@ function PlanIcon({ className, children, ...props }: PlanIconProps) {
285
285
  );
286
286
  }
287
287
 
288
- export interface PlanTitleProps extends ViewProps {
288
+ export interface PlanTitleProps extends TextProps {
289
289
  className?: string;
290
290
  children?: ReactNode;
291
291
  }
@@ -306,7 +306,7 @@ function PlanTitle({ className, children, ...props }: PlanTitleProps) {
306
306
  );
307
307
  }
308
308
 
309
- export interface PlanDescriptionProps extends ViewProps {
309
+ export interface PlanDescriptionProps extends TextProps {
310
310
  className?: string;
311
311
  children?: ReactNode;
312
312
  }