panelui-native 0.66.1 → 0.71.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 (196) hide show
  1. package/README.md +31 -1
  2. package/lib/module/components/accordion/index.js +28 -9
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/area-chart/index.js +18 -1
  5. package/lib/module/components/area-chart/index.js.map +1 -1
  6. package/lib/module/components/bar-chart/index.js +18 -1
  7. package/lib/module/components/bar-chart/index.js.map +1 -1
  8. package/lib/module/components/button/index.js +14 -7
  9. package/lib/module/components/button/index.js.map +1 -1
  10. package/lib/module/components/candlestick-chart/index.js +18 -1
  11. package/lib/module/components/candlestick-chart/index.js.map +1 -1
  12. package/lib/module/components/collapsible/index.js +280 -0
  13. package/lib/module/components/collapsible/index.js.map +1 -0
  14. package/lib/module/components/flow/index.js +66 -3
  15. package/lib/module/components/flow/index.js.map +1 -1
  16. package/lib/module/components/form/index.js +7 -5
  17. package/lib/module/components/form/index.js.map +1 -1
  18. package/lib/module/components/form/typed-form.js +11 -0
  19. package/lib/module/components/form/typed-form.js.map +1 -0
  20. package/lib/module/components/form/use-form.js +4 -0
  21. package/lib/module/components/form/use-form.js.map +1 -1
  22. package/lib/module/components/heatmap-chart/index.js +27 -1
  23. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  24. package/lib/module/components/line-chart/index.js +18 -1
  25. package/lib/module/components/line-chart/index.js.map +1 -1
  26. package/lib/module/components/live-line-chart/index.js +103 -9
  27. package/lib/module/components/live-line-chart/index.js.map +1 -1
  28. package/lib/module/components/live-line-chart/live-line-accessibility.js +47 -0
  29. package/lib/module/components/live-line-chart/live-line-accessibility.js.map +1 -0
  30. package/lib/module/components/live-line-chart/live-line-lifecycle.js +33 -0
  31. package/lib/module/components/live-line-chart/live-line-lifecycle.js.map +1 -0
  32. package/lib/module/components/marquee/index.js +194 -0
  33. package/lib/module/components/marquee/index.js.map +1 -0
  34. package/lib/module/components/message-scroller/index.js +172 -10
  35. package/lib/module/components/message-scroller/index.js.map +1 -1
  36. package/lib/module/components/message-scroller/message-scroller-math.js +23 -0
  37. package/lib/module/components/message-scroller/message-scroller-math.js.map +1 -1
  38. package/lib/module/components/meter/index.js +268 -0
  39. package/lib/module/components/meter/index.js.map +1 -0
  40. package/lib/module/components/meter/meter-scale.js +142 -0
  41. package/lib/module/components/meter/meter-scale.js.map +1 -0
  42. package/lib/module/components/planner/index.js +788 -0
  43. package/lib/module/components/planner/index.js.map +1 -0
  44. package/lib/module/components/planner/planner-announcement.js +30 -0
  45. package/lib/module/components/planner/planner-announcement.js.map +1 -0
  46. package/lib/module/components/planner/planner-entries.js +132 -0
  47. package/lib/module/components/planner/planner-entries.js.map +1 -0
  48. package/lib/module/components/planner/planner-grid-navigation.js +33 -0
  49. package/lib/module/components/planner/planner-grid-navigation.js.map +1 -0
  50. package/lib/module/components/planner/planner-lifecycle.js +45 -0
  51. package/lib/module/components/planner/planner-lifecycle.js.map +1 -0
  52. package/lib/module/components/popover/index.js +2 -2
  53. package/lib/module/components/popover/index.js.map +1 -1
  54. package/lib/module/components/radar-chart/index.js +18 -1
  55. package/lib/module/components/radar-chart/index.js.map +1 -1
  56. package/lib/module/components/scatter-chart/index.js +18 -1
  57. package/lib/module/components/scatter-chart/index.js.map +1 -1
  58. package/lib/module/components/skeleton/index.js +45 -7
  59. package/lib/module/components/skeleton/index.js.map +1 -1
  60. package/lib/module/components/spinner/index.js +63 -14
  61. package/lib/module/components/spinner/index.js.map +1 -1
  62. package/lib/module/components/time-picker/index.js +46 -21
  63. package/lib/module/components/time-picker/index.js.map +1 -1
  64. package/lib/module/components/time-picker/ruler-window.js +25 -0
  65. package/lib/module/components/time-picker/ruler-window.js.map +1 -0
  66. package/lib/module/hooks/breakpoint-contract.js +29 -0
  67. package/lib/module/hooks/breakpoint-contract.js.map +1 -0
  68. package/lib/module/hooks/index.js +1 -1
  69. package/lib/module/hooks/index.js.map +1 -1
  70. package/lib/module/hooks/use-breakpoint.js +46 -1
  71. package/lib/module/hooks/use-breakpoint.js.map +1 -1
  72. package/lib/module/index.js +5 -1
  73. package/lib/module/index.js.map +1 -1
  74. package/lib/module/primitives/chart-accessibility.js +68 -0
  75. package/lib/module/primitives/chart-accessibility.js.map +1 -0
  76. package/lib/module/primitives/focus-restoration-store.js +52 -0
  77. package/lib/module/primitives/focus-restoration-store.js.map +1 -0
  78. package/lib/module/primitives/portal.js +36 -2
  79. package/lib/module/primitives/portal.js.map +1 -1
  80. package/lib/typescript/src/components/accordion/index.d.ts +4 -0
  81. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  82. package/lib/typescript/src/components/area-chart/index.d.ts +2 -1
  83. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  84. package/lib/typescript/src/components/bar-chart/index.d.ts +2 -1
  85. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  87. package/lib/typescript/src/components/candlestick-chart/index.d.ts +2 -1
  88. package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -1
  89. package/lib/typescript/src/components/collapsible/index.d.ts +76 -0
  90. package/lib/typescript/src/components/collapsible/index.d.ts.map +1 -0
  91. package/lib/typescript/src/components/flow/index.d.ts +7 -1
  92. package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
  93. package/lib/typescript/src/components/form/index.d.ts +18 -5
  94. package/lib/typescript/src/components/form/index.d.ts.map +1 -1
  95. package/lib/typescript/src/components/form/typed-form.d.ts +7 -0
  96. package/lib/typescript/src/components/form/typed-form.d.ts.map +1 -0
  97. package/lib/typescript/src/components/form/use-form.d.ts +4 -0
  98. package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
  99. package/lib/typescript/src/components/heatmap-chart/index.d.ts +2 -1
  100. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  101. package/lib/typescript/src/components/line-chart/index.d.ts +2 -1
  102. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  103. package/lib/typescript/src/components/live-line-chart/index.d.ts +21 -7
  104. package/lib/typescript/src/components/live-line-chart/index.d.ts.map +1 -1
  105. package/lib/typescript/src/components/live-line-chart/live-line-accessibility.d.ts +22 -0
  106. package/lib/typescript/src/components/live-line-chart/live-line-accessibility.d.ts.map +1 -0
  107. package/lib/typescript/src/components/live-line-chart/live-line-lifecycle.d.ts +23 -0
  108. package/lib/typescript/src/components/live-line-chart/live-line-lifecycle.d.ts.map +1 -0
  109. package/lib/typescript/src/components/marquee/index.d.ts +73 -0
  110. package/lib/typescript/src/components/marquee/index.d.ts.map +1 -0
  111. package/lib/typescript/src/components/message-scroller/index.d.ts +30 -2
  112. package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
  113. package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts +10 -0
  114. package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts.map +1 -1
  115. package/lib/typescript/src/components/meter/index.d.ts +273 -0
  116. package/lib/typescript/src/components/meter/index.d.ts.map +1 -0
  117. package/lib/typescript/src/components/meter/meter-scale.d.ts +96 -0
  118. package/lib/typescript/src/components/meter/meter-scale.d.ts.map +1 -0
  119. package/lib/typescript/src/components/planner/index.d.ts +274 -0
  120. package/lib/typescript/src/components/planner/index.d.ts.map +1 -0
  121. package/lib/typescript/src/components/planner/planner-announcement.d.ts +15 -0
  122. package/lib/typescript/src/components/planner/planner-announcement.d.ts.map +1 -0
  123. package/lib/typescript/src/components/planner/planner-entries.d.ts +75 -0
  124. package/lib/typescript/src/components/planner/planner-entries.d.ts.map +1 -0
  125. package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts +4 -0
  126. package/lib/typescript/src/components/planner/planner-grid-navigation.d.ts.map +1 -0
  127. package/lib/typescript/src/components/planner/planner-lifecycle.d.ts +20 -0
  128. package/lib/typescript/src/components/planner/planner-lifecycle.d.ts.map +1 -0
  129. package/lib/typescript/src/components/radar-chart/index.d.ts +4 -0
  130. package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -1
  131. package/lib/typescript/src/components/scatter-chart/index.d.ts +2 -1
  132. package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -1
  133. package/lib/typescript/src/components/skeleton/index.d.ts +25 -2
  134. package/lib/typescript/src/components/skeleton/index.d.ts.map +1 -1
  135. package/lib/typescript/src/components/spinner/index.d.ts +25 -2
  136. package/lib/typescript/src/components/spinner/index.d.ts.map +1 -1
  137. package/lib/typescript/src/components/time-picker/index.d.ts +5 -6
  138. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
  139. package/lib/typescript/src/components/time-picker/ruler-window.d.ts +12 -0
  140. package/lib/typescript/src/components/time-picker/ruler-window.d.ts.map +1 -0
  141. package/lib/typescript/src/hooks/breakpoint-contract.d.ts +6 -0
  142. package/lib/typescript/src/hooks/breakpoint-contract.d.ts.map +1 -0
  143. package/lib/typescript/src/hooks/index.d.ts +1 -1
  144. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  145. package/lib/typescript/src/hooks/use-breakpoint.d.ts +22 -0
  146. package/lib/typescript/src/hooks/use-breakpoint.d.ts.map +1 -1
  147. package/lib/typescript/src/index.d.ts +5 -1
  148. package/lib/typescript/src/index.d.ts.map +1 -1
  149. package/lib/typescript/src/primitives/chart-accessibility.d.ts +33 -0
  150. package/lib/typescript/src/primitives/chart-accessibility.d.ts.map +1 -0
  151. package/lib/typescript/src/primitives/focus-restoration-store.d.ts +24 -0
  152. package/lib/typescript/src/primitives/focus-restoration-store.d.ts.map +1 -0
  153. package/lib/typescript/src/primitives/portal.d.ts +4 -0
  154. package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
  155. package/lib/typescript/types/form-api.type-test.d.ts +2 -0
  156. package/lib/typescript/types/form-api.type-test.d.ts.map +1 -0
  157. package/package.json +63 -4
  158. package/src/components/accordion/index.tsx +31 -8
  159. package/src/components/area-chart/index.tsx +27 -2
  160. package/src/components/bar-chart/index.tsx +27 -2
  161. package/src/components/button/index.tsx +11 -4
  162. package/src/components/candlestick-chart/index.tsx +30 -2
  163. package/src/components/collapsible/index.tsx +278 -0
  164. package/src/components/flow/index.tsx +93 -14
  165. package/src/components/form/index.tsx +32 -5
  166. package/src/components/form/typed-form.ts +13 -0
  167. package/src/components/form/use-form.ts +9 -0
  168. package/src/components/heatmap-chart/index.tsx +49 -3
  169. package/src/components/line-chart/index.tsx +27 -2
  170. package/src/components/live-line-chart/index.tsx +130 -13
  171. package/src/components/live-line-chart/live-line-accessibility.ts +61 -0
  172. package/src/components/live-line-chart/live-line-lifecycle.ts +63 -0
  173. package/src/components/marquee/index.tsx +232 -0
  174. package/src/components/message-scroller/index.tsx +234 -6
  175. package/src/components/message-scroller/message-scroller-math.ts +38 -0
  176. package/src/components/meter/index.tsx +341 -0
  177. package/src/components/meter/meter-scale.ts +185 -0
  178. package/src/components/planner/index.tsx +997 -0
  179. package/src/components/planner/planner-announcement.ts +37 -0
  180. package/src/components/planner/planner-entries.ts +155 -0
  181. package/src/components/planner/planner-grid-navigation.ts +45 -0
  182. package/src/components/planner/planner-lifecycle.ts +76 -0
  183. package/src/components/popover/index.tsx +3 -3
  184. package/src/components/radar-chart/index.tsx +24 -0
  185. package/src/components/scatter-chart/index.tsx +27 -2
  186. package/src/components/skeleton/index.tsx +59 -5
  187. package/src/components/spinner/index.tsx +80 -13
  188. package/src/components/time-picker/index.tsx +57 -29
  189. package/src/components/time-picker/ruler-window.ts +24 -0
  190. package/src/hooks/breakpoint-contract.ts +31 -0
  191. package/src/hooks/index.ts +4 -0
  192. package/src/hooks/use-breakpoint.ts +61 -10
  193. package/src/index.ts +43 -0
  194. package/src/primitives/chart-accessibility.ts +102 -0
  195. package/src/primitives/focus-restoration-store.ts +70 -0
  196. package/src/primitives/portal.tsx +34 -2
@@ -0,0 +1,341 @@
1
+ import { forwardRef, useEffect } from 'react';
2
+ import { View, type LayoutChangeEvent, type ViewProps } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useReducedMotion,
6
+ useSharedValue,
7
+ withSpring,
8
+ withTiming,
9
+ type SharedValue,
10
+ } from 'react-native-reanimated';
11
+ import { tv, type VariantProps } from 'tailwind-variants';
12
+ import { Text } from '../../primitives/text';
13
+ import {
14
+ colorFor,
15
+ litSegments,
16
+ meterSemantics,
17
+ normalizeScale,
18
+ normalizeSegments,
19
+ type MeterColor,
20
+ type MeterThreshold,
21
+ } from './meter-scale';
22
+
23
+ const SPRING = {
24
+ damping: 20,
25
+ stiffness: 180,
26
+ mass: 0.6,
27
+ overshootClamping: true,
28
+ } as const;
29
+ /** Milliseconds for a segment to light or go out. */
30
+ const SEGMENT_DURATION = 180;
31
+ /** How faint an unlit segment sits. Present, but plainly not counted. */
32
+ const SEGMENT_UNLIT = 0.22;
33
+
34
+ const meterVariants = tv({
35
+ slots: {
36
+ root: 'w-full gap-2',
37
+ header: 'flex-row items-center justify-between gap-2',
38
+ track: 'w-full overflow-hidden rounded-full',
39
+ indicator: 'h-full rounded-full',
40
+ segments: 'w-full flex-row overflow-hidden',
41
+ segment: 'flex-1 rounded-full',
42
+ },
43
+ variants: {
44
+ color: {
45
+ primary: { track: 'bg-primary/16', indicator: 'bg-primary', segment: 'bg-primary' },
46
+ success: { track: 'bg-success/16', indicator: 'bg-success', segment: 'bg-success' },
47
+ warning: { track: 'bg-warning/16', indicator: 'bg-warning', segment: 'bg-warning' },
48
+ destructive: {
49
+ track: 'bg-destructive/16',
50
+ indicator: 'bg-destructive',
51
+ segment: 'bg-destructive',
52
+ },
53
+ info: { track: 'bg-info/16', indicator: 'bg-info', segment: 'bg-info' },
54
+ muted: {
55
+ track: 'bg-muted',
56
+ indicator: 'bg-muted-foreground',
57
+ segment: 'bg-muted-foreground',
58
+ },
59
+ },
60
+ size: {
61
+ sm: { track: 'h-1.5', segments: 'h-1.5 gap-1', segment: 'h-1.5' },
62
+ md: { track: 'h-2', segments: 'h-2 gap-1.5', segment: 'h-2' },
63
+ lg: { track: 'h-3', segments: 'h-3 gap-1.5', segment: 'h-3' },
64
+ },
65
+ },
66
+ defaultVariants: {
67
+ color: 'primary',
68
+ size: 'md',
69
+ },
70
+ });
71
+
72
+ type MeterVariantProps = VariantProps<typeof meterVariants>;
73
+
74
+ export interface MeterProps
75
+ extends Omit<ViewProps, 'children'>,
76
+ MeterVariantProps {
77
+ className?: string;
78
+ /** The measurement. Values outside the scale are clamped to its ends. */
79
+ value: number;
80
+ /**
81
+ * The bottom of the scale — the value at which the bar reads as empty.
82
+ * Defaults to `0`.
83
+ */
84
+ minValue?: number;
85
+ /**
86
+ * The top of the scale — the value at which the bar reads as full. Defaults
87
+ * to `100`, so a bare percentage needs neither bound set.
88
+ */
89
+ maxValue?: number;
90
+ /**
91
+ * What is being measured. Also the accessibility label, and a meter without
92
+ * one announces a number with nothing attached to it — "seventy-five
93
+ * percent" of what is not an answer. Pass `accessibilityLabel` instead only
94
+ * where the caption is drawn some other way.
95
+ */
96
+ label?: string;
97
+ /** Draw the value above the track, opposite the label. */
98
+ showValueLabel?: boolean;
99
+ /**
100
+ * Text for the value label. Overrides the formatted value — use it for a
101
+ * word where a number reads worse: `Strong`, `Almost full`.
102
+ *
103
+ * It is spoken whether or not it is drawn, so it can give a screen reader
104
+ * better words than the caption without changing the caption. Pair it with
105
+ * `showValueLabel` to draw it too.
106
+ */
107
+ valueLabel?: string;
108
+ /**
109
+ * How to write the value, through `Intl.NumberFormat`. A `percent` style
110
+ * formats how far up the scale the value sits; every other style formats
111
+ * the value itself, so `{ style: 'unit', unit: 'gigabyte' }` against a
112
+ * `maxValue` of `256` reads `64 GB` rather than a percentage of it. Falls
113
+ * back to a rounded percent when omitted.
114
+ */
115
+ formatOptions?: Intl.NumberFormatOptions;
116
+ /**
117
+ * Points on the scale where the colour changes, each `{ from, color }`. The
118
+ * highest one the reading has reached wins, so the order you list them in
119
+ * does not matter; below all of them the `color` prop applies. Non-finite
120
+ * `from` values are ignored.
121
+ *
122
+ * This is the difference between a meter and a bar: the colour is a
123
+ * judgement about the reading. Which direction is bad is yours to say —
124
+ * thresholds climbing to `destructive` suit a disk filling up, thresholds
125
+ * falling to it suit a battery running down.
126
+ */
127
+ thresholds?: MeterThreshold[];
128
+ /**
129
+ * Draw the scale as this many discrete blocks instead of one continuous
130
+ * bar. Blocks are all or nothing, which is the point of them: four blocks
131
+ * say "three out of four" where a bar says "about seventy percent", and a
132
+ * password is not seventy percent strong.
133
+ *
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.
138
+ */
139
+ segments?: number;
140
+ /** Extra classes for the fill, or for a lit segment. */
141
+ indicatorClassName?: string;
142
+ /** Extra classes for the label + value row. */
143
+ headerClassName?: string;
144
+ }
145
+
146
+ interface SegmentProps {
147
+ index: number;
148
+ /** How many blocks are lit, as a shared value so lighting stays off the JS thread. */
149
+ lit: SharedValue<number>;
150
+ className: string;
151
+ reducedMotion: boolean;
152
+ }
153
+
154
+ /**
155
+ * One block of a segmented meter. It reads the lit count rather than a boolean
156
+ * so the whole row animates from one shared value, without a render per block.
157
+ */
158
+ function Segment({ index, lit, className, reducedMotion }: SegmentProps) {
159
+ const style = useAnimatedStyle(() => {
160
+ const on = lit.value > index ? 1 : SEGMENT_UNLIT;
161
+ return {
162
+ opacity: reducedMotion
163
+ ? on
164
+ : withTiming(on, { duration: SEGMENT_DURATION }),
165
+ };
166
+ });
167
+
168
+ return <Animated.View style={style} className={className} />;
169
+ }
170
+
171
+ /**
172
+ * A measurement on a fixed scale: disk used, battery left, a score, a password's
173
+ * strength. The fill is driven on the UI thread, so a value change costs one
174
+ * render and nothing per frame.
175
+ *
176
+ * A meter is not a progress bar. Progress is a task moving towards an end that
177
+ * finishing is the point of; a meter is a reading that happens to sit where it
178
+ * sits, and may go back down. Reach for
179
+ * [Progress](/docs/components/progress) when something is completing.
180
+ *
181
+ * `thresholds` paint the bar by where the reading falls, and `segments` draws
182
+ * the scale as discrete blocks for readings that are counted rather than
183
+ * measured.
184
+ *
185
+ * The value is read against `minValue` / `maxValue`, which default to 0 and
186
+ * 100 — so a percentage needs neither. Set them and the meter speaks in
187
+ * whatever is being measured: bytes, degrees, points. Nothing has to be
188
+ * converted to a percent on the way in, and the screen reader is told the real
189
+ * scale rather than a derived one.
190
+ *
191
+ * React Native has no meter role, so this announces as a progress bar with its
192
+ * value, range and unit attached. That is the closest the platform offers, and
193
+ * it is why `label` matters: the role will not distinguish a reading from a
194
+ * task, and the name is what does.
195
+ */
196
+ export const Meter = forwardRef<View, MeterProps>(
197
+ (
198
+ {
199
+ className,
200
+ indicatorClassName,
201
+ headerClassName,
202
+ value,
203
+ minValue = 0,
204
+ maxValue = 100,
205
+ label,
206
+ showValueLabel = false,
207
+ valueLabel,
208
+ formatOptions,
209
+ thresholds,
210
+ segments,
211
+ color,
212
+ size,
213
+ accessibilityLabel,
214
+ ...props
215
+ },
216
+ ref
217
+ ) => {
218
+ const scale = normalizeScale(value, minValue, maxValue);
219
+ const { value: held, fraction } = scale;
220
+ const segmentCount = normalizeSegments(segments);
221
+ const resolvedColor = colorFor(held, color ?? 'primary', thresholds);
222
+ const slots = meterVariants({ color: resolvedColor, size });
223
+ const reducedMotion = useReducedMotion();
224
+
225
+ const trackWidth = useSharedValue(0);
226
+ const progress = useSharedValue(fraction);
227
+ // Any reading above the floor lights a block, so "a little" never looks
228
+ // like "none". Rounding down would leave the first quarter of a
229
+ // four-block meter dark, which is the reading it is least able to afford.
230
+ const litCount = litSegments(fraction, segmentCount);
231
+ const lit = useSharedValue(litCount);
232
+
233
+ // Reduce motion lands on the value instead of springing to it — the
234
+ // number is the information, and the overshoot is decoration.
235
+ useEffect(() => {
236
+ progress.value = reducedMotion ? fraction : withSpring(fraction, SPRING);
237
+ }, [fraction, progress, reducedMotion]);
238
+
239
+ useEffect(() => {
240
+ lit.value = litCount;
241
+ }, [lit, litCount]);
242
+
243
+ const onLayout = (event: LayoutChangeEvent) => {
244
+ const width = event.nativeEvent.layout.width;
245
+ trackWidth.value = Number.isFinite(width) && width > 0 ? width : 0;
246
+ };
247
+
248
+ const fillStyle = useAnimatedStyle(() => ({
249
+ width: trackWidth.value * progress.value,
250
+ }));
251
+
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;
265
+ const showValue = showValueLabel;
266
+ const hasHeader = label != null || showValue;
267
+
268
+ const accessibility = {
269
+ accessibilityRole: 'progressbar' as const,
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,
274
+ };
275
+
276
+ const bar =
277
+ segmentCount > 0 ? (
278
+ <View
279
+ ref={ref}
280
+ {...accessibility}
281
+ className={slots.segments({ className })}
282
+ {...props}
283
+ >
284
+ {Array.from({ length: segmentCount }, (_, index) => (
285
+ <Segment
286
+ key={index}
287
+ index={index}
288
+ lit={lit}
289
+ reducedMotion={reducedMotion}
290
+ className={slots.segment({ className: indicatorClassName })}
291
+ />
292
+ ))}
293
+ </View>
294
+ ) : (
295
+ <View
296
+ ref={ref}
297
+ {...accessibility}
298
+ className={slots.track({ className })}
299
+ onLayout={onLayout}
300
+ {...props}
301
+ >
302
+ <Animated.View
303
+ style={fillStyle}
304
+ className={slots.indicator({ className: indicatorClassName })}
305
+ />
306
+ </View>
307
+ );
308
+
309
+ if (!hasHeader) return bar;
310
+
311
+ return (
312
+ <View className={slots.root()}>
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
+ >
320
+ {label != null ? (
321
+ <Text size="sm" weight="medium" numberOfLines={1}>
322
+ {label}
323
+ </Text>
324
+ ) : (
325
+ <View />
326
+ )}
327
+ {showValue ? (
328
+ <Text size="sm" muted numberOfLines={1}>
329
+ {spoken}
330
+ </Text>
331
+ ) : null}
332
+ </View>
333
+ {bar}
334
+ </View>
335
+ );
336
+ }
337
+ );
338
+
339
+ Meter.displayName = 'Meter';
340
+
341
+ export type { MeterColor, MeterThreshold };
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Where a reading sits on a meter's scale, and what colour it has earned.
3
+ *
4
+ * Kept apart from the component because it is the part that can be wrong
5
+ * without looking wrong: a threshold picked in the wrong order, or a segment
6
+ * count that rounds a real reading down to nothing, is a meter that lies
7
+ * quietly. None of it touches React, so it can be tested on its own.
8
+ */
9
+
10
+ /** The token a meter can be painted with. */
11
+ export type MeterColor =
12
+ | 'primary'
13
+ | 'success'
14
+ | 'warning'
15
+ | 'destructive'
16
+ | 'info'
17
+ | 'muted';
18
+
19
+ /**
20
+ * A point on the scale, and the colour the meter takes from there upwards.
21
+ *
22
+ * `from` is in the same units as `value` — not a percentage of the range — so
23
+ * a threshold on a 0–8 GB meter is written in gigabytes.
24
+ */
25
+ export interface MeterThreshold {
26
+ /** The value at which this colour takes over. */
27
+ from: number;
28
+ /** What the bar is painted with at or above `from`. */
29
+ color: MeterColor;
30
+ }
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
+
78
+ /** `value` held inside the scale, so a stray number cannot escape the track. */
79
+ export function clamp(value: number, min: number, max: number) {
80
+ return normalizeScale(value, min, max).value;
81
+ }
82
+
83
+ /**
84
+ * How far up the scale the value sits, 0–1. An empty or inverted range has no
85
+ * meaningful position in it, so it reads as empty rather than dividing by zero.
86
+ */
87
+ export function fractionOf(value: number, min: number, max: number) {
88
+ return normalizeScale(value, min, max).fraction;
89
+ }
90
+
91
+ /**
92
+ * The colour the reading has earned: the highest threshold at or below the
93
+ * value, or the base colour when it has reached none of them.
94
+ *
95
+ * Highest-wins rather than first-wins, so the caller can list thresholds in
96
+ * any order and get the same answer. A rule that depended on the order would
97
+ * make a reordered array a silent behaviour change.
98
+ */
99
+ export function colorFor(
100
+ value: number,
101
+ base: MeterColor | undefined,
102
+ thresholds: MeterThreshold[] | undefined
103
+ ) {
104
+ if (!thresholds?.length) return base;
105
+ let winner: MeterThreshold | undefined;
106
+ for (const threshold of thresholds) {
107
+ if (!Number.isFinite(threshold.from)) continue;
108
+ if (value < threshold.from) continue;
109
+ if (!winner || threshold.from > winner.from) winner = threshold;
110
+ }
111
+ return winner?.color ?? base;
112
+ }
113
+
114
+ /**
115
+ * How many blocks of a segmented meter are lit.
116
+ *
117
+ * Rounded up, so any reading above the floor lights at least one. Rounding
118
+ * down would leave the whole first block of a four-block meter dark, and "a
119
+ * little" looking like "none" is the reading a meter can least afford to get
120
+ * wrong.
121
+ */
122
+ export function litSegments(fraction: number, segments: number) {
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);
127
+ }
128
+
129
+ /**
130
+ * The value caption: an explicit override, an `Intl` rendering, or a rounded
131
+ * percent.
132
+ *
133
+ * A `percent` style is given the fraction, because that is what a percentage
134
+ * of the scale means; every other style is given the value, because a byte
135
+ * count or a score is a quantity and not a proportion.
136
+ */
137
+ export function formatValue(
138
+ value: number,
139
+ fraction: number,
140
+ valueLabel?: string,
141
+ formatOptions?: Intl.NumberFormatOptions
142
+ ) {
143
+ if (valueLabel != null) return valueLabel;
144
+ if (formatOptions) {
145
+ try {
146
+ return new Intl.NumberFormat(undefined, formatOptions).format(
147
+ formatOptions.style === 'percent' ? fraction : value
148
+ );
149
+ } catch {
150
+ // Some engines ship a partial Intl; fall through to the plain percent.
151
+ }
152
+ }
153
+ return `${Math.round(fraction * 100)}%`;
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
+ }