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
@@ -61,6 +61,7 @@ import Animated, {
61
61
  import Svg, { ClipPath, Defs, G, Line as SvgLine, Rect } from 'react-native-svg';
62
62
  import { useCSSVariable } from 'uniwind';
63
63
  import { Text } from '../../primitives/text';
64
+ import { ChartAccessibilityData, type ChartAccessibilityProps } from '../../primitives/chart-accessibility';
64
65
  import { cn } from '../../utils/cn';
65
66
  import { normalizeWeekStart, startOfDay } from '../../utils/date';
66
67
 
@@ -309,7 +310,7 @@ function deriveLevels(data: HeatmapColumn[]): number[] {
309
310
  /* Root */
310
311
  /* -------------------------------------------------------------------------- */
311
312
 
312
- export interface HeatmapChartProps extends ViewProps {
313
+ export interface HeatmapChartProps extends ViewProps, ChartAccessibilityProps<HeatmapCell> {
313
314
  className?: string;
314
315
  /** One column per period, with its row bins inside. */
315
316
  data: HeatmapColumn[];
@@ -396,6 +397,11 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
396
397
  animationDuration = 900,
397
398
  inactiveOpacity = 1,
398
399
  onActiveCellChange,
400
+ accessible,
401
+ accessibilityLabel,
402
+ accessibilityHint,
403
+ accessibilityLabelForDatum,
404
+ onAccessibilityDatumPress,
399
405
  children,
400
406
  ...props
401
407
  },
@@ -491,6 +497,20 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
491
497
  [data, levelOf]
492
498
  );
493
499
 
500
+ const accessibilityCells = useMemo(
501
+ () =>
502
+ data.flatMap((column) =>
503
+ column.bins.map((bin) => ({
504
+ column: column.bin,
505
+ row: bin.bin,
506
+ count: bin.count,
507
+ level: levelOf(bin.count),
508
+ date: bin.date,
509
+ }))
510
+ ),
511
+ [data, levelOf]
512
+ );
513
+
494
514
  const setActiveCell = useMemo(
495
515
  () => (cell: HeatmapCell | null) => {
496
516
  setActiveCellState(cell);
@@ -570,11 +590,37 @@ const HeatmapChartRoot = forwardRef<View, HeatmapChartProps>(function HeatmapCha
570
590
  whole chart, so it starts at the chart's edge, not the grid's. */}
571
591
  {parts.header}
572
592
 
593
+ <ChartAccessibilityData
594
+ chart="Heatmap chart"
595
+ data={accessibilityCells}
596
+ disabled={accessible === false}
597
+ accessibilityLabel={accessibilityLabel}
598
+ accessibilityHint={accessibilityHint}
599
+ accessibilityLabelForDatum={accessibilityLabelForDatum}
600
+ onAccessibilityDatumPress={onAccessibilityDatumPress}
601
+ valueOf={(cell) => [
602
+ ['date', cell.date],
603
+ ['column', cell.column],
604
+ ['row', cell.row],
605
+ ['count', cell.count],
606
+ ]}
607
+ />
608
+
573
609
  {parts.x ? (
574
- <View style={{ paddingLeft: axisWidth }}>{parts.x}</View>
610
+ <View
611
+ style={{ paddingLeft: axisWidth }}
612
+ accessibilityElementsHidden
613
+ importantForAccessibility="no-hide-descendants"
614
+ >
615
+ {parts.x}
616
+ </View>
575
617
  ) : null}
576
618
 
577
- <View className="flex-row">
619
+ <View
620
+ className="flex-row"
621
+ accessibilityElementsHidden
622
+ importantForAccessibility="no-hide-descendants"
623
+ >
578
624
  {parts.y}
579
625
  <View style={{ width: grid.width, height: grid.height }}>
580
626
  {grid.width > 0 ? (
@@ -82,6 +82,7 @@ import Svg, {
82
82
  } from 'react-native-svg';
83
83
  import { useCSSVariable } from 'uniwind';
84
84
  import { Text } from '../../primitives/text';
85
+ import { ChartAccessibilityData, type ChartAccessibilityProps } from '../../primitives/chart-accessibility';
85
86
  import {
86
87
  areaPath,
87
88
  columnValues,
@@ -172,7 +173,7 @@ export function useLineChart() {
172
173
  };
173
174
  }
174
175
 
175
- export interface LineChartProps extends ViewProps {
176
+ export interface LineChartProps extends ViewProps, ChartAccessibilityProps<LineChartDatum> {
176
177
  className?: string;
177
178
  /** The rows. Each one is a point along the x-axis. */
178
179
  data: LineChartDatum[];
@@ -227,6 +228,11 @@ const LineChartRoot = forwardRef<LineChartHandle, LineChartProps>(function LineC
227
228
  yDomain,
228
229
  curve = 'monotone',
229
230
  onActiveIndexChange,
231
+ accessible,
232
+ accessibilityLabel,
233
+ accessibilityHint,
234
+ accessibilityLabelForDatum,
235
+ onAccessibilityDatumPress,
230
236
  compact = false,
231
237
  children,
232
238
  ...props
@@ -443,7 +449,26 @@ const LineChartRoot = forwardRef<LineChartHandle, LineChartProps>(function LineC
443
449
  <LineChartContext.Provider value={context}>
444
450
  <View {...props} style={props.style} className={cn('w-full', className)}>
445
451
  {header}
446
- <View onLayout={onLayout} style={{ aspectRatio }} className="w-full">
452
+ <ChartAccessibilityData
453
+ chart="Line chart"
454
+ data={data}
455
+ disabled={accessible === false || loading}
456
+ accessibilityLabel={accessibilityLabel}
457
+ accessibilityHint={accessibilityHint}
458
+ accessibilityLabelForDatum={accessibilityLabelForDatum}
459
+ onAccessibilityDatumPress={onAccessibilityDatumPress}
460
+ valueOf={(datum) => [
461
+ [xDataKey, datum[xDataKey]],
462
+ ...series.map(([key]) => [key, datum[key]] as [string, unknown]),
463
+ ]}
464
+ />
465
+ <View
466
+ onLayout={onLayout}
467
+ style={{ aspectRatio }}
468
+ className="w-full"
469
+ accessibilityElementsHidden
470
+ importantForAccessibility="no-hide-descendants"
471
+ >
447
472
  {plot.width > 0 ? (
448
473
  <>
449
474
  <Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
@@ -30,10 +30,17 @@
30
30
  * they mean opposite things.
31
31
  *
32
32
  * The cost is a frame callback for as long as the chart is mounted. It is
33
- * stopped by `paused`, by `status="loading"` and on unmount, and it is never
34
- * started when the platform asks for reduced motion — in that case the window
35
- * advances as each point arrives instead, which is the same picture sampled
36
- * less often.
33
+ * stopped by `paused`, by `status="loading"`, while the app is backgrounded
34
+ * and on unmount, and it is never started when the platform asks for reduced
35
+ * motion — in that case the window advances as each point arrives instead,
36
+ * which is the same picture sampled less often.
37
+ *
38
+ * Screen readers receive one image-role snapshot: its name, current or
39
+ * selected value, direction, time window and paused state. It changes when the
40
+ * React data changes, never on the UI-thread clock frame, and requests no live
41
+ * announcement. The visual axes, badges and tooltip repeat that snapshot and
42
+ * stay out of the accessibility tree; controls placed in Header remain normal
43
+ * controls.
37
44
  *
38
45
  * ## Colour follows the recent direction
39
46
  *
@@ -56,7 +63,13 @@ import {
56
63
  useState,
57
64
  type ReactNode,
58
65
  } from 'react';
59
- import { StyleSheet, View, type LayoutChangeEvent, type ViewProps } from 'react-native';
66
+ import {
67
+ AppState,
68
+ StyleSheet,
69
+ View,
70
+ type LayoutChangeEvent,
71
+ type ViewProps,
72
+ } from 'react-native';
60
73
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
61
74
  import Animated, {
62
75
  runOnJS,
@@ -92,6 +105,13 @@ import {
92
105
  type Plot,
93
106
  } from '../../utils/chart';
94
107
  import { cn } from '../../utils/cn';
108
+ import { liveLineAccessibility } from './live-line-accessibility';
109
+ import {
110
+ liveLineClockRuns,
111
+ normalizeLiveLinePoints,
112
+ normalizeLiveLineWindow,
113
+ reconcileLiveLineActivePoint,
114
+ } from './live-line-lifecycle';
95
115
 
96
116
  const AnimatedPath = Animated.createAnimatedComponent(Path);
97
117
 
@@ -224,9 +244,16 @@ function runOf(
224
244
 
225
245
  export interface LiveLineChartProps extends ViewProps {
226
246
  className?: string;
227
- /** The readings so far, oldest first. Append to it as they arrive. */
247
+ /**
248
+ * Names the chart's single screen-reader snapshot. Falls back to the Header
249
+ * title, then to "Live line chart".
250
+ */
251
+ accessibilityLabel?: string;
252
+ /** Additional guidance after the snapshot. No gesture is invented for it. */
253
+ accessibilityHint?: string;
254
+ /** The readings so far. Invalid values are dropped and timestamps are ordered. */
228
255
  data: LiveLinePoint[];
229
- /** How much time the plot spans, in seconds. */
256
+ /** How much time the plot spans, in seconds. Invalid values use 30. */
230
257
  window?: number;
231
258
  /** Freeze the window where it is. The readings still arrive; the clock stops. */
232
259
  paused?: boolean;
@@ -239,7 +266,7 @@ export interface LiveLineChartProps extends ViewProps {
239
266
  /**
240
267
  * The most readings kept. Older ones are dropped, since they are off the
241
268
  * window and cannot come back — an unbounded feed otherwise grows an array
242
- * for as long as the screen is open.
269
+ * for as long as the screen is open. Must be positive and finite.
243
270
  */
244
271
  maxPoints?: number;
245
272
  /** Width ÷ height of the plot. */
@@ -276,6 +303,8 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
276
303
  momentumColors,
277
304
  color,
278
305
  onActivePointChange,
306
+ accessibilityLabel,
307
+ accessibilityHint,
279
308
  children,
280
309
  ...props
281
310
  },
@@ -290,12 +319,13 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
290
319
  const domainMax = useSharedValue(0);
291
320
  const activeTime = useSharedValue(-1);
292
321
  const reducedMotion = useReducedMotion();
322
+ const [appState, setAppState] = useState(AppState.currentState ?? 'active');
293
323
  // Stripped of punctuation: `useId` returns something like `:r1:`, and a
294
324
  // colon inside a `url(#…)` reference does not resolve — which in RN SVG is
295
325
  // a clip that silently does nothing rather than an error.
296
326
  const clipId = `panelui-live-clip-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
297
327
 
298
- const windowMs = Math.max(windowSeconds, 1) * 1000;
328
+ const windowMs = normalizeLiveLineWindow(windowSeconds) * 1000;
299
329
  const loading = status === 'loading';
300
330
 
301
331
  const hasYAxis = useMemo(() => {
@@ -308,6 +338,34 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
308
338
  return found;
309
339
  }, [children]);
310
340
 
341
+ const semanticParts = useMemo(() => {
342
+ let title: string | undefined;
343
+ let value: string | undefined;
344
+ let headerFormat: ((reading: number) => string) | undefined;
345
+ let tipFormat: ((reading: number) => string) | undefined;
346
+ let tooltipFormat: ((reading: number) => string) | undefined;
347
+ Children.forEach(children, (child) => {
348
+ if (!isValidElement(child)) return;
349
+ const part = (child.type as { displayName?: string }).displayName;
350
+ if (part === 'LiveLineChart.Header') {
351
+ const header = child.props as LiveLineChartHeaderProps;
352
+ title ??= header.title;
353
+ value ??= header.value;
354
+ headerFormat ??= header.formatValue;
355
+ } else if (part === 'LiveLineChart.Tip') {
356
+ tipFormat ??= (child.props as LiveLineChartTipProps).formatValue;
357
+ } else if (part === 'LiveLineChart.Tooltip') {
358
+ tooltipFormat ??= (child.props as LiveLineChartTooltipProps).formatValue;
359
+ }
360
+ });
361
+ return {
362
+ title,
363
+ value,
364
+ formatLatest: headerFormat ?? tipFormat,
365
+ formatActive: tooltipFormat ?? headerFormat ?? tipFormat,
366
+ };
367
+ }, [children]);
368
+
311
369
  const pad = { ...PADDING, left: hasYAxis ? Y_AXIS_WIDTH : PADDING.left };
312
370
  const plot: Plot = {
313
371
  left: pad.left,
@@ -322,7 +380,7 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
322
380
  * the one chart nobody bounded to be the one left running overnight.
323
381
  */
324
382
  const points = useMemo(
325
- () => (data.length > maxPoints ? data.slice(data.length - maxPoints) : data),
383
+ () => normalizeLiveLinePoints(data, maxPoints),
326
384
  [data, maxPoints]
327
385
  );
328
386
 
@@ -331,6 +389,11 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
331
389
  values.value = points.map((point) => point.value);
332
390
  }, [points, times, values]);
333
391
 
392
+ useEffect(() => {
393
+ const subscription = AppState.addEventListener('change', setAppState);
394
+ return () => subscription.remove();
395
+ }, []);
396
+
334
397
  const liveLatest = points.length ? points[points.length - 1]! : null;
335
398
 
336
399
  /*
@@ -393,7 +456,7 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
393
456
  now.value = Date.now();
394
457
  }, false);
395
458
 
396
- const running = !paused && !loading && !reducedMotion;
459
+ const running = liveLineClockRuns({ paused, loading, reducedMotion, appState });
397
460
 
398
461
  useEffect(() => {
399
462
  frame.setActive(running);
@@ -411,6 +474,13 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
411
474
  now.value = Date.now();
412
475
  }, [running, paused, points, now]);
413
476
 
477
+ // A frame callback is suspended with the app. Land on the wall clock as
478
+ // soon as it becomes active instead of showing the backgrounded window for
479
+ // one frame and relying on the platform to schedule a callback promptly.
480
+ useEffect(() => {
481
+ if (appState === 'active' && !paused) now.value = Date.now();
482
+ }, [appState, paused, now]);
483
+
414
484
  useImperativeHandle(
415
485
  ref,
416
486
  () => ({
@@ -434,6 +504,21 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
434
504
  if (!paused) heldMomentum.current = liveMomentum;
435
505
  const momentum = paused ? heldMomentum.current : liveMomentum;
436
506
 
507
+ const defaultFormat = (reading: number) => compactNumber(reading);
508
+ const semantic = liveLineAccessibility({
509
+ name: accessibilityLabel ?? semanticParts.title,
510
+ status,
511
+ latest,
512
+ activePoint,
513
+ momentum,
514
+ windowSeconds: windowMs / 1000,
515
+ paused,
516
+ now: Date.now(),
517
+ valueOverride: semanticParts.value,
518
+ formatLatest: semanticParts.formatLatest ?? defaultFormat,
519
+ formatActive: semanticParts.formatActive ?? defaultFormat,
520
+ });
521
+
437
522
  const base = useSeriesColor(color, 1);
438
523
  const successToken = useCSSVariable('--color-success');
439
524
  const destructiveToken = useCSSVariable('--color-destructive');
@@ -458,6 +543,13 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
458
543
  [onActivePointChange]
459
544
  );
460
545
 
546
+ useEffect(() => {
547
+ const next = reconcileLiveLineActivePoint(activePoint, points);
548
+ if (next === activePoint) return;
549
+ if (!next) activeTime.value = -1;
550
+ setActivePoint(next);
551
+ }, [activePoint, points, activeTime, setActivePoint]);
552
+
461
553
  const onLayout = (event: LayoutChangeEvent) => {
462
554
  const { width, height } = event.nativeEvent.layout;
463
555
  setSize((current) =>
@@ -519,8 +611,21 @@ const LiveLineChartRoot = forwardRef<LiveLineChartHandle, LiveLineChartProps>(
519
611
  return (
520
612
  <LiveLineChartContext.Provider value={context}>
521
613
  <View {...props} style={props.style} className={cn('w-full', className)}>
614
+ <View
615
+ accessible
616
+ accessibilityRole="image"
617
+ accessibilityLabel={semantic.label}
618
+ accessibilityHint={accessibilityHint}
619
+ style={{ position: 'absolute', left: -10_000, width: 1, height: 1 }}
620
+ />
522
621
  {header}
523
- <View onLayout={onLayout} style={{ aspectRatio }} className="w-full">
622
+ <View
623
+ onLayout={onLayout}
624
+ style={{ aspectRatio }}
625
+ className="w-full"
626
+ accessibilityElementsHidden
627
+ importantForAccessibility="no-hide-descendants"
628
+ >
524
629
  {plot.width > 0 ? (
525
630
  <>
526
631
  <Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
@@ -1026,6 +1131,14 @@ function LiveLineChartTooltip({ formatValue, className }: LiveLineChartTooltipPr
1026
1131
  [setActivePoint]
1027
1132
  );
1028
1133
 
1134
+ useEffect(
1135
+ () => () => {
1136
+ activeTime.value = -1;
1137
+ setActivePoint(null);
1138
+ },
1139
+ [activeTime, setActivePoint]
1140
+ );
1141
+
1029
1142
  const pan = useMemo(() => {
1030
1143
  const resolve = (x: number) => {
1031
1144
  'worklet';
@@ -1259,7 +1372,11 @@ function LiveLineChartHeader({
1259
1372
  {...props}
1260
1373
  className={cn('flex-row items-start justify-between gap-3 pb-3', className)}
1261
1374
  >
1262
- <View className="flex-1 gap-0.5">
1375
+ <View
1376
+ className="flex-1 gap-0.5"
1377
+ accessibilityElementsHidden
1378
+ importantForAccessibility="no-hide-descendants"
1379
+ >
1263
1380
  {title ? (
1264
1381
  <Text size="xs" muted>
1265
1382
  {title}
@@ -0,0 +1,61 @@
1
+ export interface LiveLineAccessiblePoint {
2
+ time: number;
3
+ value: number;
4
+ }
5
+
6
+ export interface LiveLineAccessibilityInput {
7
+ name?: string;
8
+ status: 'loading' | 'ready';
9
+ latest: LiveLineAccessiblePoint | null;
10
+ activePoint: LiveLineAccessiblePoint | null;
11
+ momentum: 'up' | 'down' | 'flat';
12
+ windowSeconds: number;
13
+ paused: boolean;
14
+ now: number;
15
+ valueOverride?: string;
16
+ formatLatest: (value: number) => string;
17
+ formatActive: (value: number) => string;
18
+ }
19
+
20
+ /** One bounded snapshot for a stream, rather than one node per arriving point. */
21
+ export function liveLineAccessibility({
22
+ name,
23
+ status,
24
+ latest,
25
+ activePoint,
26
+ momentum,
27
+ windowSeconds,
28
+ paused,
29
+ now,
30
+ valueOverride,
31
+ formatLatest,
32
+ formatActive,
33
+ }: LiveLineAccessibilityInput) {
34
+ const parts = [name?.trim() || 'Live line chart'];
35
+
36
+ if (status === 'loading') {
37
+ parts.push('Loading');
38
+ if (paused) parts.push('Paused');
39
+ return { label: parts.join('. ') };
40
+ }
41
+
42
+ if (!latest) {
43
+ parts.push('No readings');
44
+ if (paused) parts.push('Paused');
45
+ return { label: parts.join('. ') };
46
+ }
47
+
48
+ if (activePoint) {
49
+ const secondsAgo = Math.max(0, Math.round((now - activePoint.time) / 1000));
50
+ parts.push(`Selected value, ${formatActive(activePoint.value)}`);
51
+ parts.push(secondsAgo === 0 ? 'Just now' : `${secondsAgo} seconds ago`);
52
+ } else {
53
+ parts.push(`Current value, ${valueOverride ?? formatLatest(latest.value)}`);
54
+ const direction = momentum === 'up' ? 'rising' : momentum === 'down' ? 'falling' : 'steady';
55
+ parts.push(`Trend, ${direction}`);
56
+ }
57
+
58
+ parts.push(`${windowSeconds}-second window`);
59
+ if (paused) parts.push('Paused');
60
+ return { label: parts.join('. ') };
61
+ }
@@ -0,0 +1,63 @@
1
+ import type { AppStateStatus } from 'react-native';
2
+
3
+ export interface LiveLineReading {
4
+ time: number;
5
+ value: number;
6
+ }
7
+
8
+ export const DEFAULT_LIVE_LINE_WINDOW = 30;
9
+ export const DEFAULT_LIVE_LINE_MAX_POINTS = 500;
10
+
11
+ export function normalizeLiveLineWindow(seconds: number): number {
12
+ return Number.isFinite(seconds) && seconds > 0
13
+ ? Math.max(seconds, 1)
14
+ : DEFAULT_LIVE_LINE_WINDOW;
15
+ }
16
+
17
+ export function normalizeLiveLineMaxPoints(maxPoints: number): number {
18
+ return Number.isFinite(maxPoints) && maxPoints > 0
19
+ ? Math.max(1, Math.floor(maxPoints))
20
+ : DEFAULT_LIVE_LINE_MAX_POINTS;
21
+ }
22
+
23
+ /**
24
+ * Builds the one ordered, finite buffer every renderer and interaction reads.
25
+ * For duplicate timestamps the last input wins, matching a corrected reading
26
+ * arriving with the same identity as the one it replaces.
27
+ */
28
+ export function normalizeLiveLinePoints<T extends LiveLineReading>(
29
+ data: readonly T[],
30
+ maxPoints: number,
31
+ ): T[] {
32
+ const byTime = new Map<number, T>();
33
+ for (const point of data) {
34
+ if (Number.isFinite(point.time) && Number.isFinite(point.value)) {
35
+ byTime.set(point.time, point);
36
+ }
37
+ }
38
+ return [...byTime.values()]
39
+ .sort((left, right) => left.time - right.time)
40
+ .slice(-normalizeLiveLineMaxPoints(maxPoints));
41
+ }
42
+
43
+ export function liveLineClockRuns(options: {
44
+ paused: boolean;
45
+ loading: boolean;
46
+ reducedMotion: boolean;
47
+ appState: AppStateStatus;
48
+ }): boolean {
49
+ return (
50
+ !options.paused &&
51
+ !options.loading &&
52
+ !options.reducedMotion &&
53
+ options.appState === 'active'
54
+ );
55
+ }
56
+
57
+ export function reconcileLiveLineActivePoint<T extends LiveLineReading>(
58
+ active: T | null,
59
+ points: readonly T[],
60
+ ): T | null {
61
+ if (!active) return null;
62
+ return points.find((point) => point.time === active.time) ?? null;
63
+ }