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
@@ -36,12 +36,11 @@
36
36
  *
37
37
  * ## Scroll offset, not gesture maths
38
38
  *
39
- * The wheel, the clock's list and the ruler are all scroll views with
40
- * `snapToOffsets`, and the selection is `Math.round(offset / itemSize)`. That
41
- * buys momentum, deceleration, edge bounce and platform-correct fling physics
42
- * for nothing, and none of it would be worth rebuilding on a pan gesture. The
43
- * per-item fade and the ruler's readout follow the offset on the UI thread, so
44
- * scrolling never round-trips through React — only the settled value does.
39
+ * The wheel, the clock's list and the ruler are snapping scroll views, and the
40
+ * selection is `Math.round(offset / itemSize)`. That buys momentum,
41
+ * deceleration, edge bounce and platform-correct fling physics for nothing,
42
+ * and none of it would be worth rebuilding on a pan gesture. The ruler keeps
43
+ * that full scroll range but mounts only an overscanned window of its ticks.
45
44
  */
46
45
  import {
47
46
  useCallback,
@@ -79,6 +78,7 @@ import {
79
78
  accessibilityValueForIndex,
80
79
  indexForAccessibilityAction,
81
80
  } from './accessibility';
81
+ import { rulerWindow } from './ruler-window';
82
82
  import {
83
83
  clampTime,
84
84
  displayHour,
@@ -848,10 +848,20 @@ function RulerFace({
848
848
  */
849
849
  const [width, setWidth] = useState(0);
850
850
  const pad = Math.max(0, width / 2 - TICK_SPACING / 2);
851
+ const [windowIndex, setWindowIndex] = useState(index);
852
+ // A controlled/accessibility jump renders its destination before the effect
853
+ // moves the scroll view there; an in-flight gesture follows the scroll window.
854
+ const windowAnchor = index === resting.current ? windowIndex : index;
855
+ const window = rulerWindow(times.length, windowAnchor);
851
856
 
852
- const snapToOffsets = useMemo(
853
- () => times.map((_, i) => i * TICK_SPACING),
854
- [times]
857
+ const showIndex = useCallback(
858
+ (next: number) => {
859
+ const nextStart = rulerWindow(times.length, next).start;
860
+ setWindowIndex((current) =>
861
+ rulerWindow(times.length, current).start === nextStart ? current : next
862
+ );
863
+ },
864
+ [times.length]
855
865
  );
856
866
 
857
867
  // The scale runs the same rest-and-resync machinery the wheel's columns do,
@@ -872,10 +882,14 @@ function RulerFace({
872
882
 
873
883
  // Unanimated, for the reason given on the wheel's columns: an animated
874
884
  // programmatic scroll reports itself as a gesture and the correction loops.
875
- const snapTo = useCallback((to: number) => {
876
- resting.current = to;
877
- ref.current?.scrollTo({ x: to * TICK_SPACING, animated: false });
878
- }, []);
885
+ const snapTo = useCallback(
886
+ (to: number) => {
887
+ resting.current = to;
888
+ showIndex(to);
889
+ ref.current?.scrollTo({ x: to * TICK_SPACING, animated: false });
890
+ },
891
+ [showIndex]
892
+ );
879
893
 
880
894
  useEffect(() => {
881
895
  if (index === resting.current) return;
@@ -934,6 +948,13 @@ function RulerFace({
934
948
  [atRest, cancelTimer, settleAt]
935
949
  );
936
950
 
951
+ const onScroll = useCallback(
952
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
953
+ showIndex(Math.round(event.nativeEvent.contentOffset.x / TICK_SPACING));
954
+ },
955
+ [showIndex]
956
+ );
957
+
937
958
  useEffect(() => cancelTimer, [cancelTimer]);
938
959
 
939
960
  const stepsPerHour = Math.max(1, Math.round(60 / minuteStep));
@@ -985,9 +1006,11 @@ function RulerFace({
985
1006
  contentOffset={{ x: index * TICK_SPACING, y: 0 }}
986
1007
  scrollEnabled={!disabled}
987
1008
  showsHorizontalScrollIndicator={false}
988
- snapToOffsets={snapToOffsets}
1009
+ snapToInterval={TICK_SPACING}
989
1010
  disableIntervalMomentum
990
1011
  decelerationRate="fast"
1012
+ onScroll={onScroll}
1013
+ scrollEventThrottle={16}
991
1014
  onScrollBeginDrag={startMoving}
992
1015
  onScrollEndDrag={onDragEnd}
993
1016
  onMomentumScrollBegin={startMoving}
@@ -1009,22 +1032,27 @@ function RulerFace({
1009
1032
  at most — which is a legend, not a scale, and reads as clutter
1010
1033
  beside a number set at 36px.
1011
1034
  */}
1012
- {times.map((_, i) => (
1013
- <View
1014
- key={i}
1015
- style={{ width: TICK_SPACING }}
1016
- className="items-center justify-center"
1017
- >
1035
+ <View style={{ width: window.start * TICK_SPACING }} />
1036
+ {times.slice(window.start, window.end).map((_, offset) => {
1037
+ const i = window.start + offset;
1038
+ return (
1018
1039
  <View
1019
- className={cn(
1020
- 'w-0.5 rounded-full',
1021
- // Taller and darker on the hour, so the scale reads as hours
1022
- // rather than as an undifferentiated comb.
1023
- i % stepsPerHour === 0 ? 'h-7 bg-muted-foreground' : 'h-4 bg-border'
1024
- )}
1025
- />
1026
- </View>
1027
- ))}
1040
+ key={i}
1041
+ style={{ width: TICK_SPACING }}
1042
+ className="items-center justify-center"
1043
+ >
1044
+ <View
1045
+ className={cn(
1046
+ 'w-0.5 rounded-full',
1047
+ // Taller and darker on the hour, so the scale reads as hours
1048
+ // rather than as an undifferentiated comb.
1049
+ i % stepsPerHour === 0 ? 'h-7 bg-muted-foreground' : 'h-4 bg-border'
1050
+ )}
1051
+ />
1052
+ </View>
1053
+ );
1054
+ })}
1055
+ <View style={{ width: (times.length - window.end) * TICK_SPACING }} />
1028
1056
  </ScrollView>
1029
1057
 
1030
1058
  {/* Over the scale rather than in it: the indicator marks the centre of
@@ -0,0 +1,24 @@
1
+ /** Maximum tick cells mounted by the ruler, regardless of the minute step. */
2
+ export const RULER_WINDOW_SIZE = 121;
3
+
4
+ const WINDOW_SHIFT = 40;
5
+
6
+ export interface RulerWindow {
7
+ start: number;
8
+ end: number;
9
+ }
10
+
11
+ /**
12
+ * A chunked, overscanned slice around the visible tick. It moves only once per
13
+ * WINDOW_SHIFT ticks, leaving at least forty ticks ahead of a normal scroll.
14
+ */
15
+ export function rulerWindow(itemCount: number, visibleIndex: number): RulerWindow {
16
+ const count = Math.max(0, Math.floor(itemCount));
17
+ if (count <= RULER_WINDOW_SIZE) return { start: 0, end: count };
18
+
19
+ const index = Math.min(Math.max(Math.round(visibleIndex), 0), count - 1);
20
+ const chunk = Math.floor(index / WINDOW_SHIFT) * WINDOW_SHIFT;
21
+ const before = Math.floor((RULER_WINDOW_SIZE - WINDOW_SHIFT) / 2);
22
+ const start = Math.min(Math.max(chunk - before, 0), count - RULER_WINDOW_SIZE);
23
+ return { start, end: start + RULER_WINDOW_SIZE };
24
+ }
@@ -0,0 +1,31 @@
1
+ export type BreakpointDefinition = Readonly<Record<string, number>>;
2
+
3
+ /** Validate declaration order once, when an app creates its responsive contract. */
4
+ export function breakpointEntries<const T extends BreakpointDefinition>(definition: T) {
5
+ const entries = Object.entries(definition) as [keyof T & string, number][];
6
+ if (!entries.length) throw new Error('Breakpoints must declare at least one threshold.');
7
+ let previous = -1;
8
+ for (const [name, threshold] of entries) {
9
+ if (!name || !Number.isFinite(threshold) || threshold < 0) {
10
+ throw new Error(`Breakpoint "${name}" must have a finite, non-negative threshold.`);
11
+ }
12
+ if (threshold <= previous) {
13
+ throw new Error('Breakpoint thresholds must be strictly ascending in declaration order.');
14
+ }
15
+ previous = threshold;
16
+ }
17
+ return entries;
18
+ }
19
+
20
+ /** Largest satisfied name, with `base` as the deterministic below-range fallback. */
21
+ export function breakpointAt<Name extends string>(
22
+ entries: readonly (readonly [Name, number])[],
23
+ width: number
24
+ ): Name | 'base' {
25
+ let current: Name | 'base' = 'base';
26
+ for (const [name, threshold] of entries) {
27
+ if (width < threshold) break;
28
+ current = name;
29
+ }
30
+ return current;
31
+ }
@@ -10,8 +10,12 @@ export {
10
10
  } from './use-disclosure';
11
11
  export {
12
12
  useBreakpoint,
13
+ createBreakpoints,
13
14
  BREAKPOINTS,
14
15
  type Breakpoint,
16
+ type BreakpointContract,
17
+ type BreakpointProviderProps,
18
+ type BreakpointResult,
15
19
  type UseBreakpointResult,
16
20
  } from './use-breakpoint';
17
21
  export { useKeyboard, type UseKeyboardResult } from './use-keyboard';
@@ -1,7 +1,14 @@
1
+ import { createContext, createElement, useContext, type ReactNode } from 'react';
1
2
  import { useWindowDimensions } from 'react-native';
3
+ import {
4
+ breakpointAt,
5
+ breakpointEntries,
6
+ type BreakpointDefinition,
7
+ } from './breakpoint-contract';
2
8
 
3
9
  /** Tailwind's breakpoints, minus the ones no phone or tablet reaches. */
4
10
  export const BREAKPOINTS = { sm: 640, md: 768, lg: 1024, xl: 1280 } as const;
11
+ const DEFAULT_ENTRIES = breakpointEntries(BREAKPOINTS);
5
12
 
6
13
  export type Breakpoint = keyof typeof BREAKPOINTS;
7
14
 
@@ -15,6 +22,59 @@ export interface UseBreakpointResult {
15
22
  isLandscape: boolean;
16
23
  }
17
24
 
25
+ export interface BreakpointResult<Name extends string> {
26
+ current: Name | 'base';
27
+ isAtLeast: (breakpoint: Name) => boolean;
28
+ width: number;
29
+ height: number;
30
+ isLandscape: boolean;
31
+ }
32
+
33
+ export interface BreakpointProviderProps {
34
+ children?: ReactNode;
35
+ }
36
+
37
+ export interface BreakpointContract<Name extends string> {
38
+ Provider: (props: BreakpointProviderProps) => ReactNode;
39
+ useBreakpoint: () => BreakpointResult<Name>;
40
+ breakpoints: Readonly<Record<Name, number>>;
41
+ }
42
+
43
+ /**
44
+ * Create app-specific responsive names. One Provider owns the window
45
+ * subscription; every bound hook below it reads the same dimensions.
46
+ */
47
+ export function createBreakpoints<const T extends BreakpointDefinition>(
48
+ definition: T
49
+ ): BreakpointContract<keyof T & string> {
50
+ type Name = keyof T & string;
51
+ const breakpoints = Object.freeze({ ...definition }) as Readonly<T>;
52
+ const entries = breakpointEntries(breakpoints);
53
+ const Context = createContext<{ width: number; height: number } | null>(null);
54
+
55
+ function Provider({ children }: BreakpointProviderProps) {
56
+ const dimensions = useWindowDimensions();
57
+ return createElement(Context.Provider, { value: dimensions }, children);
58
+ }
59
+
60
+ function useBoundBreakpoint(): BreakpointResult<Name> {
61
+ const dimensions = useContext(Context);
62
+ if (!dimensions) {
63
+ throw new Error('This breakpoint hook must be used inside its Breakpoint Provider.');
64
+ }
65
+ const { width, height } = dimensions;
66
+ return {
67
+ current: breakpointAt(entries, width),
68
+ isAtLeast: (name) => width >= breakpoints[name]!,
69
+ width,
70
+ height,
71
+ isLandscape: width > height,
72
+ };
73
+ }
74
+
75
+ return { Provider, useBreakpoint: useBoundBreakpoint, breakpoints };
76
+ }
77
+
18
78
  /**
19
79
  * Responsive state from the window size — the React Native answer to a media
20
80
  * query, which has no equivalent here.
@@ -31,16 +91,7 @@ export interface UseBreakpointResult {
31
91
  export function useBreakpoint(): UseBreakpointResult {
32
92
  const { width, height } = useWindowDimensions();
33
93
 
34
- const current: Breakpoint | 'base' =
35
- width >= BREAKPOINTS.xl
36
- ? 'xl'
37
- : width >= BREAKPOINTS.lg
38
- ? 'lg'
39
- : width >= BREAKPOINTS.md
40
- ? 'md'
41
- : width >= BREAKPOINTS.sm
42
- ? 'sm'
43
- : 'base';
94
+ const current = breakpointAt(DEFAULT_ENTRIES, width);
44
95
 
45
96
  return {
46
97
  current,
package/src/index.ts CHANGED
@@ -416,6 +416,14 @@ export {
416
416
  type ChipVariant,
417
417
  type ChipSize,
418
418
  } from './components/chip';
419
+ export {
420
+ Collapsible,
421
+ type CollapsibleProps,
422
+ type CollapsibleTriggerProps,
423
+ type CollapsibleIndicatorProps,
424
+ type CollapsibleContentProps,
425
+ type CollapsibleVariant,
426
+ } from './components/collapsible';
419
427
  export {
420
428
  ColorPicker,
421
429
  type ColorPickerProps,
@@ -485,10 +493,14 @@ export {
485
493
  } from './components/field';
486
494
  export {
487
495
  Form,
496
+ createForm,
488
497
  useForm,
489
498
  useField,
490
499
  type FormProps,
491
500
  type FormFieldProps,
501
+ type TypedForm,
502
+ type TypedFormProps,
503
+ type TypedFormFieldProps,
492
504
  type FormApi,
493
505
  type FieldErrors,
494
506
  type FieldTouched,
@@ -724,6 +736,11 @@ export {
724
736
  type MarkerIconProps,
725
737
  type MarkerContentProps,
726
738
  } from './components/marker';
739
+ export {
740
+ Marquee,
741
+ type MarqueeProps,
742
+ type MarqueeDirection,
743
+ } from './components/marquee';
727
744
  export {
728
745
  Menu,
729
746
  type MenuProps,
@@ -742,6 +759,12 @@ export {
742
759
  type MenuItemVariant,
743
760
  type MenuRadioIndicator,
744
761
  } from './components/menu';
762
+ export {
763
+ Meter,
764
+ type MeterProps,
765
+ type MeterColor,
766
+ type MeterThreshold,
767
+ } from './components/meter';
745
768
  export {
746
769
  MessageScroller,
747
770
  useMessageScroller,
@@ -779,6 +802,26 @@ export {
779
802
  type PopoverAnchorRect,
780
803
  type PopoverAnchorControls,
781
804
  } from './components/popover';
805
+ export {
806
+ Planner,
807
+ type PlannerProps,
808
+ type PlannerEntry,
809
+ type PlannerCategory,
810
+ type PlannerHeaderProps,
811
+ type PlannerTitleProps,
812
+ type PlannerTodayProps,
813
+ type PlannerNavProps,
814
+ type PlannerActionProps,
815
+ type PlannerGridProps,
816
+ type PlannerDayProps,
817
+ type PlannerDayState,
818
+ type PlannerDayRenderer,
819
+ type PlannerLegendProps,
820
+ type PlannerSummaryProps,
821
+ type PlannerFooterProps,
822
+ type PlannerDetailsProps,
823
+ type PlannerCountedCategory,
824
+ } from './components/planner';
782
825
  export {
783
826
  Post,
784
827
  type PostProps,
@@ -0,0 +1,102 @@
1
+ import { createElement } from 'react';
2
+
3
+ export interface ChartAccessibilityProps<Datum> {
4
+ /** Spoken summary. Defaults to the chart family and number of data entries. */
5
+ accessibilityLabel?: string;
6
+ /** Additional guidance spoken after the chart summary. */
7
+ accessibilityHint?: string;
8
+ /** Overrides the spoken label for one data entry. */
9
+ accessibilityLabelForDatum?: (datum: Datum, index: number) => string;
10
+ /** Makes each data entry actionable without making the SVG geometry focusable. */
11
+ onAccessibilityDatumPress?: (datum: Datum, index: number) => void;
12
+ }
13
+
14
+ export interface ChartAccessibilityItem<Datum> {
15
+ datum: Datum;
16
+ index: number;
17
+ label: string;
18
+ }
19
+
20
+ function spoken(value: unknown): string | null {
21
+ if (value instanceof Date) return value.toLocaleDateString();
22
+ if (typeof value === 'string' || typeof value === 'number') return String(value);
23
+ return null;
24
+ }
25
+
26
+ /** Pure semantic projection, shared by tests and the native accessibility tree. */
27
+ export function chartAccessibilityModel<Datum>(
28
+ chart: string,
29
+ data: Datum[],
30
+ valueOf: (datum: Datum) => [string, unknown][],
31
+ labelForDatum?: (datum: Datum, index: number) => string
32
+ ): { summary: string; items: ChartAccessibilityItem<Datum>[] } {
33
+ return {
34
+ summary: `${chart}, ${data.length} ${data.length === 1 ? 'data entry' : 'data entries'}`,
35
+ items: data.map((datum, index) => ({
36
+ datum,
37
+ index,
38
+ label:
39
+ labelForDatum?.(datum, index) ??
40
+ valueOf(datum)
41
+ .map(([label, value]) => {
42
+ const text = spoken(value);
43
+ return text === null ? null : `${label}, ${text}`;
44
+ })
45
+ .filter((part): part is string => part !== null)
46
+ .join('. '),
47
+ })),
48
+ };
49
+ }
50
+
51
+ export interface ChartAccessibilityDataProps<Datum>
52
+ extends ChartAccessibilityProps<Datum> {
53
+ chart: string;
54
+ data: Datum[];
55
+ valueOf: (datum: Datum) => [string, unknown][];
56
+ disabled?: boolean;
57
+ }
58
+
59
+ /**
60
+ * A compact semantic sibling to the visual plot. The SVG remains one
61
+ * decorative subtree while screen readers receive one summary and one entry
62
+ * per datum, rather than every path, grid line and marker.
63
+ */
64
+ export function ChartAccessibilityData<Datum>({
65
+ chart,
66
+ data,
67
+ valueOf,
68
+ disabled = false,
69
+ accessibilityLabel,
70
+ accessibilityHint,
71
+ accessibilityLabelForDatum,
72
+ onAccessibilityDatumPress,
73
+ }: ChartAccessibilityDataProps<Datum>) {
74
+ if (disabled) return null;
75
+ // Keep the pure semantic projection importable by Node tests without asking
76
+ // Node to parse React Native's Flow entry point.
77
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
78
+ const { Pressable, View } = require('react-native') as typeof import('react-native');
79
+ const model = chartAccessibilityModel(chart, data, valueOf, accessibilityLabelForDatum);
80
+
81
+ return createElement(
82
+ View,
83
+ { style: { position: 'absolute', left: -10_000, width: 1, height: 1 } },
84
+ createElement(View, {
85
+ accessible: true,
86
+ accessibilityRole: 'image',
87
+ accessibilityLabel: accessibilityLabel ?? model.summary,
88
+ accessibilityHint,
89
+ }),
90
+ ...model.items.map((item) =>
91
+ createElement(Pressable, {
92
+ key: item.index,
93
+ accessible: true,
94
+ accessibilityRole: onAccessibilityDatumPress ? 'button' : 'text',
95
+ accessibilityLabel: item.label,
96
+ onPress: onAccessibilityDatumPress
97
+ ? () => onAccessibilityDatumPress(item.datum, item.index)
98
+ : undefined,
99
+ })
100
+ )
101
+ );
102
+ }
@@ -0,0 +1,70 @@
1
+ interface FocusDocument {
2
+ activeElement?: FocusTarget | null;
3
+ body?: FocusTarget | null;
4
+ documentElement?: FocusTarget | null;
5
+ }
6
+
7
+ interface FocusTarget {
8
+ disabled?: boolean;
9
+ isConnected?: boolean;
10
+ focus: (options?: { preventScroll?: boolean }) => void;
11
+ getAttribute?: (name: string) => string | null;
12
+ }
13
+
14
+ type RestoreFocus = () => boolean;
15
+
16
+ interface FocusOwner {
17
+ owner: string;
18
+ released: boolean;
19
+ restore: RestoreFocus;
20
+ }
21
+
22
+ /** Restores nested overlays in last-opened order, even when parents close first. */
23
+ export class FocusRestorationStore {
24
+ private owners: FocusOwner[] = [];
25
+
26
+ acquire(owner: string, restore: RestoreFocus): (() => void) {
27
+ const entry = { owner, released: false, restore };
28
+ this.owners.push(entry);
29
+
30
+ return () => {
31
+ if (entry.released) return;
32
+ entry.released = true;
33
+ this.restoreReleasedOwners();
34
+ };
35
+ }
36
+
37
+ private restoreReleasedOwners() {
38
+ const candidates: RestoreFocus[] = [];
39
+ while (this.owners.at(-1)?.released) {
40
+ const entry = this.owners.pop();
41
+ if (entry) candidates.push(entry.restore);
42
+ }
43
+ candidates.some((restore) => restore());
44
+ }
45
+ }
46
+
47
+ /** Captures the current browser focus without evaluating DOM APIs on native. */
48
+ export function captureFocusRestore(
49
+ document = (globalThis as { document?: FocusDocument }).document
50
+ ): RestoreFocus {
51
+ const target = document?.activeElement;
52
+ if (!target || target === document?.body || target === document?.documentElement) {
53
+ return () => false;
54
+ }
55
+
56
+ return () => {
57
+ if (target.isConnected === false || target.disabled) return false;
58
+ if (target.getAttribute?.('aria-disabled') === 'true') return false;
59
+ try {
60
+ target.focus({ preventScroll: true });
61
+ } catch {
62
+ try {
63
+ target.focus();
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+ return document?.activeElement === target;
69
+ };
70
+ }
@@ -8,6 +8,7 @@ import {
8
8
  type ReactNode,
9
9
  } from 'react';
10
10
  import { ModalIsolationStore } from './modal-isolation-store';
11
+ import { captureFocusRestore, FocusRestorationStore } from './focus-restoration-store';
11
12
 
12
13
  type PortalMap = ReadonlyMap<string, ReactNode>;
13
14
 
@@ -41,16 +42,21 @@ class PortalStore {
41
42
 
42
43
  const PortalContext = createContext<PortalStore | null>(null);
43
44
  const ModalIsolationContext = createContext<ModalIsolationStore | null>(null);
45
+ const FocusRestorationContext = createContext<FocusRestorationStore | null>(null);
44
46
 
45
47
  export function PortalProvider({ children }: { children: ReactNode }) {
46
48
  const portalStoreRef = useRef<PortalStore | null>(null);
47
49
  const modalStoreRef = useRef<ModalIsolationStore | null>(null);
50
+ const focusStoreRef = useRef<FocusRestorationStore | null>(null);
48
51
  portalStoreRef.current ??= new PortalStore();
49
52
  modalStoreRef.current ??= new ModalIsolationStore();
53
+ focusStoreRef.current ??= new FocusRestorationStore();
50
54
  return (
51
55
  <PortalContext.Provider value={portalStoreRef.current}>
52
56
  <ModalIsolationContext.Provider value={modalStoreRef.current}>
53
- {children}
57
+ <FocusRestorationContext.Provider value={focusStoreRef.current}>
58
+ {children}
59
+ </FocusRestorationContext.Provider>
54
60
  </ModalIsolationContext.Provider>
55
61
  </PortalContext.Provider>
56
62
  );
@@ -80,9 +86,27 @@ export function Portal({ children }: { children: ReactNode }) {
80
86
  /** A portal that owns modal focus until it unmounts. */
81
87
  export function ModalPortal({ children }: { children: ReactNode }) {
82
88
  const store = useModalIsolationStore('ModalPortal');
89
+ const focusStore = useFocusRestorationStore('ModalPortal');
83
90
  const owner = useId();
84
91
 
85
- useEffect(() => store.acquire(owner), [owner, store]);
92
+ useEffect(() => {
93
+ const releaseIsolation = store.acquire(owner);
94
+ const releaseFocus = focusStore.acquire(owner, captureFocusRestore());
95
+ return () => {
96
+ releaseIsolation();
97
+ releaseFocus();
98
+ };
99
+ }, [focusStore, owner, store]);
100
+
101
+ return <Portal>{children}</Portal>;
102
+ }
103
+
104
+ /** A non-isolating portal that returns browser focus to what opened it. */
105
+ export function FocusRestorePortal({ children }: { children: ReactNode }) {
106
+ const store = useFocusRestorationStore('FocusRestorePortal');
107
+ const owner = useId();
108
+
109
+ useEffect(() => store.acquire(owner, captureFocusRestore()), [owner, store]);
86
110
 
87
111
  return <Portal>{children}</Portal>;
88
112
  }
@@ -101,6 +125,14 @@ function useModalIsolationStore(component: string): ModalIsolationStore {
101
125
  return store;
102
126
  }
103
127
 
128
+ function useFocusRestorationStore(component: string): FocusRestorationStore {
129
+ const store = useContext(FocusRestorationContext);
130
+ if (!store) {
131
+ throw new Error(`${component} must be used within a <PanelUIProvider>`);
132
+ }
133
+ return store;
134
+ }
135
+
104
136
  /** Mount point for portaled content. PanelUIProvider renders one automatically. */
105
137
  export function PortalHost() {
106
138
  const store = usePortalStore('PortalHost');