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
@@ -70,6 +70,7 @@ import Animated, {
70
70
  import Svg, { G, Line as SvgLine, Path } from 'react-native-svg';
71
71
  import { useCSSVariable } from 'uniwind';
72
72
  import { Text } from '../../primitives/text';
73
+ import { ChartAccessibilityData, type ChartAccessibilityProps } from '../../primitives/chart-accessibility';
73
74
  import { bandOf, compactNumber, yOf, type Plot } from '../../utils/chart';
74
75
  import { cn } from '../../utils/cn';
75
76
 
@@ -234,7 +235,7 @@ function ohlcOf(
234
235
  return { open, high, low, close };
235
236
  }
236
237
 
237
- export interface CandlestickChartProps extends ViewProps {
238
+ export interface CandlestickChartProps extends ViewProps, ChartAccessibilityProps<CandlestickChartDatum> {
238
239
  className?: string;
239
240
  /** The rows. Each one is a period along the x-axis. */
240
241
  data: CandlestickChartDatum[];
@@ -333,6 +334,11 @@ const CandlestickChartRoot = forwardRef<CandlestickChartHandle, CandlestickChart
333
334
  candleWidth,
334
335
  fadedOpacity = 0.3,
335
336
  onActiveIndexChange,
337
+ accessible,
338
+ accessibilityLabel,
339
+ accessibilityHint,
340
+ accessibilityLabelForDatum,
341
+ onAccessibilityDatumPress,
336
342
  compact = false,
337
343
  children,
338
344
  ...props
@@ -532,7 +538,29 @@ const CandlestickChartRoot = forwardRef<CandlestickChartHandle, CandlestickChart
532
538
  <CandlestickChartContext.Provider value={context}>
533
539
  <View {...props} style={props.style} className={cn('w-full', className)}>
534
540
  {header}
535
- <View onLayout={onLayout} style={{ aspectRatio }} className="w-full">
541
+ <ChartAccessibilityData
542
+ chart="Candlestick chart"
543
+ data={data}
544
+ disabled={accessible === false || loading}
545
+ accessibilityLabel={accessibilityLabel}
546
+ accessibilityHint={accessibilityHint}
547
+ accessibilityLabelForDatum={accessibilityLabelForDatum}
548
+ onAccessibilityDatumPress={onAccessibilityDatumPress}
549
+ valueOf={(datum) => [
550
+ [xDataKey, datum[xDataKey]],
551
+ [openDataKey, datum[openDataKey]],
552
+ [highDataKey, datum[highDataKey]],
553
+ [lowDataKey, datum[lowDataKey]],
554
+ [closeDataKey, datum[closeDataKey]],
555
+ ]}
556
+ />
557
+ <View
558
+ onLayout={onLayout}
559
+ style={{ aspectRatio }}
560
+ className="w-full"
561
+ accessibilityElementsHidden
562
+ importantForAccessibility="no-hide-descendants"
563
+ >
536
564
  {plot.width > 0 ? (
537
565
  <>
538
566
  <Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Collapsible — one section of content that a header shows and hides.
3
+ *
4
+ * ```tsx
5
+ * <Collapsible variant="surface">
6
+ * <Collapsible.Trigger>
7
+ * Delivery options
8
+ * <Collapsible.Indicator />
9
+ * </Collapsible.Trigger>
10
+ * <Collapsible.Content>
11
+ * <Text>Anything, of any height.</Text>
12
+ * </Collapsible.Content>
13
+ * </Collapsible>
14
+ * ```
15
+ *
16
+ * ## Why this and not a one-item Accordion
17
+ *
18
+ * The two look the same and differ in what happens to the body when it closes.
19
+ * An accordion unmounts it, which is right when a closed section should cost
20
+ * nothing and there is nothing inside worth keeping.
21
+ *
22
+ * This one keeps the body mounted and animates its height, so what is inside
23
+ * survives being closed — a part-filled form still has what was typed into it,
24
+ * a list is still scrolled to where it was, a video is still part-way through.
25
+ * That is the case this component exists for; reach for `Accordion` when
26
+ * sections are mutually exclusive or the body is expensive and rarely opened.
27
+ *
28
+ * ## The body is mounted whether or not it is open
29
+ *
30
+ * A height cannot be animated from `auto`, so the content has to be measured
31
+ * before it can be travelled to, and measuring it means rendering it. There is
32
+ * no lazy version of that: a body that has never been opened has still been
33
+ * rendered once. For a body heavy enough that this matters, an `Accordion` with
34
+ * a single item unmounts it instead.
35
+ *
36
+ * ## Reduced motion
37
+ *
38
+ * With the operating system set to reduce motion the panel snaps between its
39
+ * two states and the chevron turns without travelling. The disclosure still
40
+ * happens — it is the movement that is dropped, not the change.
41
+ */
42
+ import {
43
+ createContext,
44
+ forwardRef,
45
+ useCallback,
46
+ useContext,
47
+ useEffect,
48
+ useMemo,
49
+ useState,
50
+ type ReactNode,
51
+ } from 'react';
52
+ import { Pressable, View, type Text as RNText, type ViewProps } from 'react-native';
53
+ import Animated, {
54
+ useAnimatedStyle,
55
+ useReducedMotion,
56
+ useSharedValue,
57
+ withTiming,
58
+ } from 'react-native-reanimated';
59
+ import { tv } from 'tailwind-variants';
60
+ import { ChevronDownIcon } from '../../icons';
61
+ import { Collapse } from '../../primitives/collapse';
62
+ import { Text, type TextProps, textChildren } from '../../primitives/text';
63
+
64
+ export type CollapsibleVariant = 'default' | 'surface' | 'ghost';
65
+
66
+ /** Matches the chevron in `Accordion`, so the two read as the same control. */
67
+ const INDICATOR_DURATION = 200;
68
+
69
+ const collapsibleVariants = tv({
70
+ slots: {
71
+ root: 'w-full flex-col',
72
+ trigger: 'flex-row items-center justify-between gap-4 py-4',
73
+ title: 'flex-1 text-base font-medium text-foreground',
74
+ indicator: 'items-center justify-center',
75
+ content: 'pb-4',
76
+ contentText: 'text-sm text-muted-foreground',
77
+ },
78
+ variants: {
79
+ variant: {
80
+ default: { trigger: 'px-1', content: 'px-1' },
81
+ surface: {
82
+ root: 'overflow-hidden rounded-2xl border border-border bg-surface',
83
+ trigger: 'px-4',
84
+ content: 'px-4',
85
+ },
86
+ ghost: { trigger: 'px-0', content: 'px-0' },
87
+ },
88
+ },
89
+ defaultVariants: {
90
+ variant: 'default',
91
+ },
92
+ });
93
+
94
+ interface CollapsibleContextValue {
95
+ open: boolean;
96
+ toggle: () => void;
97
+ isDisabled: boolean;
98
+ variant: CollapsibleVariant;
99
+ }
100
+
101
+ const CollapsibleContext = createContext<CollapsibleContextValue | null>(null);
102
+
103
+ function useCollapsible(component: string): CollapsibleContextValue {
104
+ const context = useContext(CollapsibleContext);
105
+ if (!context) throw new Error(`${component} must be used within a <Collapsible>`);
106
+ return context;
107
+ }
108
+
109
+ export interface CollapsibleProps extends ViewProps {
110
+ className?: string;
111
+ variant?: CollapsibleVariant;
112
+ /** Whether the body is showing, controlled. */
113
+ open?: boolean;
114
+ defaultOpen?: boolean;
115
+ onOpenChange?: (open: boolean) => void;
116
+ /** Stops the trigger opening or closing it, and marks it disabled to a screen reader. */
117
+ isDisabled?: boolean;
118
+ children?: ReactNode;
119
+ }
120
+
121
+ const CollapsibleRoot = forwardRef<View, CollapsibleProps>(
122
+ (
123
+ {
124
+ className,
125
+ variant = 'default',
126
+ open,
127
+ defaultOpen = false,
128
+ onOpenChange,
129
+ isDisabled = false,
130
+ children,
131
+ ...props
132
+ },
133
+ ref
134
+ ) => {
135
+ const [internal, setInternal] = useState(defaultOpen);
136
+ const isControlled = open !== undefined;
137
+ const isOpen = isControlled ? open : internal;
138
+
139
+ const toggle = useCallback(() => {
140
+ const next = !isOpen;
141
+ if (!isControlled) setInternal(next);
142
+ onOpenChange?.(next);
143
+ }, [isOpen, isControlled, onOpenChange]);
144
+
145
+ const context = useMemo(
146
+ () => ({ open: isOpen, toggle, isDisabled, variant }),
147
+ [isOpen, toggle, isDisabled, variant]
148
+ );
149
+
150
+ const { root } = collapsibleVariants({ variant });
151
+
152
+ return (
153
+ <CollapsibleContext.Provider value={context}>
154
+ <View ref={ref} className={root({ className })} {...props}>
155
+ {children}
156
+ </View>
157
+ </CollapsibleContext.Provider>
158
+ );
159
+ }
160
+ );
161
+ CollapsibleRoot.displayName = 'Collapsible';
162
+
163
+ export interface CollapsibleTriggerProps extends ViewProps {
164
+ className?: string;
165
+ children?: ReactNode;
166
+ }
167
+
168
+ /** The pressable header row. Bare strings are wrapped in the title style. */
169
+ const CollapsibleTrigger = forwardRef<View, CollapsibleTriggerProps>(
170
+ ({ className, children, ...props }, ref) => {
171
+ const { open, toggle, isDisabled, variant } = useCollapsible('Collapsible.Trigger');
172
+ const { trigger, title } = collapsibleVariants({ variant });
173
+
174
+ return (
175
+ <Pressable
176
+ ref={ref}
177
+ accessibilityRole="button"
178
+ accessibilityState={{ expanded: open, disabled: isDisabled }}
179
+ disabled={isDisabled}
180
+ onPress={toggle}
181
+ className={trigger({ className })}
182
+ {...props}
183
+ >
184
+ {textChildren(children, (text) => (
185
+ <Text className={title()}>{text}</Text>
186
+ ))}
187
+ </Pressable>
188
+ );
189
+ }
190
+ );
191
+ CollapsibleTrigger.displayName = 'Collapsible.Trigger';
192
+
193
+ const CollapsibleTitle = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
194
+ const { variant } = useCollapsible('Collapsible.Title');
195
+ const { title } = collapsibleVariants({ variant });
196
+ return <Text ref={ref} className={title({ className })} {...props} />;
197
+ });
198
+ CollapsibleTitle.displayName = 'Collapsible.Title';
199
+
200
+ export interface CollapsibleIndicatorProps extends ViewProps {
201
+ className?: string;
202
+ /** Replaces the default chevron. */
203
+ children?: ReactNode;
204
+ }
205
+
206
+ /** Chevron that rotates 180° while the body is open. */
207
+ const CollapsibleIndicator = forwardRef<View, CollapsibleIndicatorProps>(
208
+ ({ className, children, ...props }, ref) => {
209
+ const { open, variant } = useCollapsible('Collapsible.Indicator');
210
+ const { indicator } = collapsibleVariants({ variant });
211
+ const reducedMotion = useReducedMotion();
212
+ const progress = useSharedValue(open ? 1 : 0);
213
+
214
+ useEffect(() => {
215
+ progress.value = reducedMotion
216
+ ? open
217
+ ? 1
218
+ : 0
219
+ : withTiming(open ? 1 : 0, { duration: INDICATOR_DURATION });
220
+ }, [open, reducedMotion, progress]);
221
+
222
+ const style = useAnimatedStyle(() => ({
223
+ transform: [{ rotate: `${progress.value * 180}deg` }],
224
+ }));
225
+
226
+ return (
227
+ <Animated.View style={style}>
228
+ <View ref={ref} className={indicator({ className })} {...props}>
229
+ {children ?? <ChevronDownIcon size={18} />}
230
+ </View>
231
+ </Animated.View>
232
+ );
233
+ }
234
+ );
235
+ CollapsibleIndicator.displayName = 'Collapsible.Indicator';
236
+
237
+ export interface CollapsibleContentProps extends ViewProps {
238
+ className?: string;
239
+ children?: ReactNode;
240
+ }
241
+
242
+ /**
243
+ * The body, opened and closed by animating its own height.
244
+ *
245
+ * It stays mounted while closed, which is the point of the component — see the
246
+ * note at the top of the file. A closed body is hidden from the accessibility
247
+ * tree as well as from the eye, because a screen reader reading out a section
248
+ * that is not on screen is worse than one that skips it.
249
+ */
250
+ const CollapsibleContent = forwardRef<View, CollapsibleContentProps>(
251
+ ({ className, children, ...props }, ref) => {
252
+ const { open, variant } = useCollapsible('Collapsible.Content');
253
+ const { content, contentText } = collapsibleVariants({ variant });
254
+
255
+ return (
256
+ <Collapse open={open} className={content({ className })}>
257
+ <View
258
+ ref={ref}
259
+ accessibilityElementsHidden={!open}
260
+ importantForAccessibility={open ? 'auto' : 'no-hide-descendants'}
261
+ {...props}
262
+ >
263
+ {textChildren(children, (text) => (
264
+ <Text className={contentText()}>{text}</Text>
265
+ ))}
266
+ </View>
267
+ </Collapse>
268
+ );
269
+ }
270
+ );
271
+ CollapsibleContent.displayName = 'Collapsible.Content';
272
+
273
+ export const Collapsible = Object.assign(CollapsibleRoot, {
274
+ Trigger: CollapsibleTrigger,
275
+ Title: CollapsibleTitle,
276
+ Indicator: CollapsibleIndicator,
277
+ Content: CollapsibleContent,
278
+ });
@@ -31,6 +31,10 @@
31
31
  * the edges are ordinary elements and cost a render per drag frame. That is a
32
32
  * real cost and it is the right trade.
33
33
  *
34
+ * An `animated` edge keeps that arrangement and animates the one property the
35
+ * geometry does not own: the dash offset. Its `d` still arrives by re-render,
36
+ * so a dragged node reshapes the edge while the dashes keep marching.
37
+ *
34
38
  * JavaScript is told a drag has finished through `onNodeDragEnd`. Positions
35
39
  * are otherwise yours to leave alone: pass `position` once and the canvas
36
40
  * takes it from there, or keep it in state and pass it back to drive nodes
@@ -65,10 +69,12 @@ import {
65
69
  import { useCSSVariable } from 'uniwind';
66
70
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
67
71
  import Animated, {
72
+ cancelAnimation,
68
73
  runOnJS,
69
74
  useAnimatedProps,
70
75
  useAnimatedStyle,
71
76
  useDerivedValue,
77
+ useReducedMotion,
72
78
  useSharedValue,
73
79
  withRepeat,
74
80
  withTiming,
@@ -159,6 +165,15 @@ const CONNECT_RADIUS = 44;
159
165
  */
160
166
  const GROUP_EDGE_STANDOFF = 5;
161
167
 
168
+ /** Dash and gap length for a broken edge, in graph points. */
169
+ const EDGE_DASH = 6;
170
+
171
+ /**
172
+ * How long an animated edge takes to travel one dash-and-gap. Slow enough to
173
+ * read as flow rather than flicker, fast enough to look live.
174
+ */
175
+ const EDGE_MARCH_DURATION = 600;
176
+
162
177
  export type FlowEdgeVariant = 'bezier' | 'smoothstep' | 'step' | 'straight';
163
178
 
164
179
  export interface FlowViewport {
@@ -1496,7 +1511,9 @@ export interface FlowEdgeProps {
1496
1511
  variant?: FlowEdgeVariant;
1497
1512
  /**
1498
1513
  * Mark the edge as carrying something — a request, a build, a dependency
1499
- * that is live rather than declared. Draws it dashed.
1514
+ * that is live rather than declared. Draws it dashed and marches the dashes
1515
+ * from source to target. Falls back to a still dashed edge when the
1516
+ * operating system is set to reduce motion.
1500
1517
  */
1501
1518
  animated?: boolean;
1502
1519
  /** Draw it broken rather than solid. */
@@ -1551,6 +1568,63 @@ function FlowEdge(props: FlowEdgeProps) {
1551
1568
  return null;
1552
1569
  }
1553
1570
 
1571
+ /**
1572
+ * A dashed edge whose dashes travel from source to target.
1573
+ *
1574
+ * The dash offset is the only animated property here, and the geometry is
1575
+ * deliberately not one: `d` arrives as an ordinary prop and changes by
1576
+ * re-render when a node moves. A path in React Native draws reliably when one
1577
+ * property animates and the rest are plain, so splitting them this way lets a
1578
+ * dragged node reshape the edge without ever interrupting the march.
1579
+ *
1580
+ * One dash and one gap of travel per cycle, which is why the loop is seamless:
1581
+ * the end state is the start state shifted by exactly one period.
1582
+ */
1583
+ function FlowMarchingEdge({
1584
+ d,
1585
+ stroke,
1586
+ width,
1587
+ }: {
1588
+ d: string;
1589
+ stroke: string;
1590
+ width: number;
1591
+ }) {
1592
+ const offset = useSharedValue(0);
1593
+ const reducedMotion = useReducedMotion();
1594
+
1595
+ useEffect(() => {
1596
+ if (reducedMotion) {
1597
+ offset.value = 0;
1598
+ return undefined;
1599
+ }
1600
+ // Negative, so the dashes run the way the path was drawn: source to target.
1601
+ offset.value = withRepeat(
1602
+ withTiming(-EDGE_DASH * 2, {
1603
+ duration: EDGE_MARCH_DURATION,
1604
+ easing: Easing.linear,
1605
+ }),
1606
+ -1,
1607
+ false
1608
+ );
1609
+ return () => cancelAnimation(offset);
1610
+ }, [offset, reducedMotion]);
1611
+
1612
+ const dashProps = useAnimatedProps(() => ({ strokeDashoffset: offset.value }));
1613
+
1614
+ return (
1615
+ <AnimatedPath
1616
+ animatedProps={dashProps}
1617
+ d={d}
1618
+ fill="none"
1619
+ stroke={stroke}
1620
+ strokeWidth={width}
1621
+ strokeLinecap="round"
1622
+ strokeLinejoin="round"
1623
+ strokeDasharray={`${EDGE_DASH} ${EDGE_DASH}`}
1624
+ />
1625
+ );
1626
+ }
1627
+
1554
1628
  /** The part that actually draws an edge, inside the canvas's one `<Svg>`. */
1555
1629
  function FlowEdgePath({
1556
1630
  from,
@@ -1612,7 +1686,7 @@ function FlowEdgePath({
1612
1686
  const b = { x: outB.x + origin.x, y: outB.y + origin.y };
1613
1687
 
1614
1688
  const d = edgePath(variant, a, sideA, b, sideB, curvature, radius, gap);
1615
- const dash = dashed || animated ? 6 : 0;
1689
+ const dash = dashed || animated ? EDGE_DASH : 0;
1616
1690
  const stroke = color ?? tint;
1617
1691
 
1618
1692
  const head = arrow
@@ -1624,15 +1698,19 @@ function FlowEdgePath({
1624
1698
 
1625
1699
  return (
1626
1700
  <>
1627
- <Path
1628
- d={d}
1629
- fill="none"
1630
- stroke={stroke}
1631
- strokeWidth={width}
1632
- strokeLinecap="round"
1633
- strokeLinejoin="round"
1634
- strokeDasharray={dash ? `${dash} ${dash}` : undefined}
1635
- />
1701
+ {animated ? (
1702
+ <FlowMarchingEdge d={d} stroke={stroke} width={width} />
1703
+ ) : (
1704
+ <Path
1705
+ d={d}
1706
+ fill="none"
1707
+ stroke={stroke}
1708
+ strokeWidth={width}
1709
+ strokeLinecap="round"
1710
+ strokeLinejoin="round"
1711
+ strokeDasharray={dash ? `${dash} ${dash}` : undefined}
1712
+ />
1713
+ )}
1636
1714
  {head ? <Path d={head} fill={stroke} /> : null}
1637
1715
  </>
1638
1716
  );
@@ -1703,9 +1781,10 @@ function FlowEdgeLayer() {
1703
1781
  origin={origin}
1704
1782
  />
1705
1783
  ))}
1706
- {/* The connection line is the one path that does animate, because it
1707
- follows a finger and has nothing else to follow. It animates `d` and
1708
- only `d`, which is the one thing that works. */}
1784
+ {/* The connection line follows a finger, so it is the path that animates
1785
+ `d` — and only `d`, which is the one thing that works while the
1786
+ geometry is what moves. An animated edge inverts the split: static
1787
+ `d`, animated dash offset. Either is fine; both at once is not. */}
1709
1788
  <AnimatedPath
1710
1789
  animatedProps={lineProps}
1711
1790
  fill="none"
@@ -7,11 +7,8 @@
7
7
  * take differently-shaped change props, and only the caller knows which one
8
8
  * a given field needs to wire up.
9
9
  *
10
- * `Form`/`Form.Field` trade some type precision for that JSX ergonomics — a
11
- * field's `name` isn't checked against `keyof YourValues` here, because a
12
- * value's shape can't flow through `useContext` generically. Reach for
13
- * `useField(form, name)` directly when a field's name should be checked
14
- * against the form's own value type.
10
+ * `createForm<Values>()` binds that value shape to the same runtime components
11
+ * when field names and render values should stay typed through JSX.
15
12
  *
16
13
  * ```tsx
17
14
  * const form = useForm({
@@ -37,6 +34,7 @@
37
34
  import { createContext, useContext, type ReactNode } from 'react';
38
35
  import { useField, type FormFieldRenderProps, type UseFieldOptions } from './use-field';
39
36
  import { useForm, type FormApi, type FieldErrors, type FieldTouched, type FieldState, type UseFormOptions, type Validator } from './use-form';
37
+ import { bindFormRuntime } from './typed-form';
40
38
 
41
39
  export { useForm, useField };
42
40
  export type {
@@ -57,6 +55,11 @@ export interface FormProps {
57
55
  children?: ReactNode;
58
56
  }
59
57
 
58
+ export interface TypedFormProps<T extends Record<string, any>> {
59
+ form: FormApi<T>;
60
+ children?: ReactNode;
61
+ }
62
+
60
63
  function FormRoot({ form, children }: FormProps) {
61
64
  return <FormContext.Provider value={form}>{children}</FormContext.Provider>;
62
65
  }
@@ -70,6 +73,14 @@ export interface FormFieldProps {
70
73
  children: (field: FormFieldRenderProps<any>) => ReactNode;
71
74
  }
72
75
 
76
+ export interface TypedFormFieldProps<
77
+ T extends Record<string, any>,
78
+ K extends keyof T,
79
+ > extends UseFieldOptions<T, K> {
80
+ name: K;
81
+ children: (field: FormFieldRenderProps<T[K]>) => ReactNode;
82
+ }
83
+
73
84
  function FormField({ name, validate, validateOn, children }: FormFieldProps) {
74
85
  const form = useContext(FormContext);
75
86
  if (!form) {
@@ -83,3 +94,19 @@ FormField.displayName = 'Form.Field';
83
94
  export const Form = Object.assign(FormRoot, {
84
95
  Field: FormField,
85
96
  });
97
+
98
+ export interface TypedForm<T extends Record<string, any>> {
99
+ (props: TypedFormProps<T>): ReactNode;
100
+ Field: <K extends keyof T>(props: TypedFormFieldProps<T, K>) => ReactNode;
101
+ useForm: (options: UseFormOptions<T>) => FormApi<T>;
102
+ useField: <K extends keyof T>(
103
+ form: FormApi<T>,
104
+ name: K,
105
+ options?: UseFieldOptions<T, K>
106
+ ) => FormFieldRenderProps<T[K]>;
107
+ }
108
+
109
+ /** Bind a value shape to Form's existing runtime without creating new state. */
110
+ export function createForm<T extends Record<string, any>>(): TypedForm<T> {
111
+ return bindFormRuntime(FormRoot, FormField, useForm, useField) as TypedForm<T>;
112
+ }
@@ -0,0 +1,13 @@
1
+ /** Attach bound helpers to the existing runtime components; no state is cloned. */
2
+ export function bindFormRuntime<
3
+ Root,
4
+ Field,
5
+ UseForm,
6
+ UseField,
7
+ >(root: Root, field: Field, useForm: UseForm, useField: UseField) {
8
+ return Object.assign(root as object, { Field: field, useForm, useField }) as Root & {
9
+ Field: Field;
10
+ useForm: UseForm;
11
+ useField: UseField;
12
+ };
13
+ }
@@ -67,6 +67,10 @@ export interface FormApi<T extends Record<string, any>> {
67
67
  setFieldValue: <K extends keyof T>(name: K, value: T[K]) => void;
68
68
  setFieldTouched: <K extends keyof T>(name: K, touched?: boolean) => void;
69
69
  setFieldError: <K extends keyof T>(name: K, error: string | undefined) => void;
70
+ /** Read the latest value, including a change made before React renders again. */
71
+ getValue: <K extends keyof T>(name: K) => T[K];
72
+ /** Read a field from this render so a component naturally follows its changes. */
73
+ watch: <K extends keyof T>(name: K) => T[K];
70
74
  getFieldState: <K extends keyof T>(name: K) => FieldState<T[K]>;
71
75
  /** Used by `useField`/`Form.Field` to attach a field's validation rule. */
72
76
  registerValidator: <K extends keyof T>(name: K, validator?: Validator<T, K>) => void;
@@ -249,6 +253,9 @@ export function useForm<T extends Record<string, any>>({
249
253
  [state.values, state.errors, state.touched]
250
254
  );
251
255
 
256
+ const getValue = useCallback(<K extends keyof T>(name: K): T[K] => valuesRef.current[name], []);
257
+ const watch = useCallback(<K extends keyof T>(name: K): T[K] => state.values[name], [state.values]);
258
+
252
259
  const validateField = useCallback(async <K extends keyof T>(name: K) => {
253
260
  const validator = validatorsRef.current[name];
254
261
  if (!validator) return true;
@@ -369,6 +376,8 @@ export function useForm<T extends Record<string, any>>({
369
376
  setFieldValue,
370
377
  setFieldTouched,
371
378
  setFieldError,
379
+ getValue,
380
+ watch,
372
381
  getFieldState,
373
382
  registerValidator,
374
383
  validateField,