@delacour/react-native-ui 0.1.0-alpha.3

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 (273) hide show
  1. package/README.md +113 -0
  2. package/package.json +125 -0
  3. package/src/components/accordion/AGENTS.md +192 -0
  4. package/src/components/accordion/accordion-content.tsx +125 -0
  5. package/src/components/accordion/accordion-description.tsx +17 -0
  6. package/src/components/accordion/accordion-indicator.tsx +104 -0
  7. package/src/components/accordion/accordion-item.tsx +78 -0
  8. package/src/components/accordion/accordion-title.tsx +18 -0
  9. package/src/components/accordion/accordion-trigger.tsx +148 -0
  10. package/src/components/accordion/accordion.context.tsx +153 -0
  11. package/src/components/accordion/accordion.tsx +253 -0
  12. package/src/components/accordion/accordion.types.ts +11 -0
  13. package/src/components/accordion/accordion.variants.test.ts +435 -0
  14. package/src/components/accordion/accordion.variants.ts +358 -0
  15. package/src/components/accordion/index.ts +45 -0
  16. package/src/components/badge/AGENTS.md +83 -0
  17. package/src/components/badge/badge-close-button.tsx +48 -0
  18. package/src/components/badge/badge-end-content.tsx +15 -0
  19. package/src/components/badge/badge-label.tsx +24 -0
  20. package/src/components/badge/badge-start-content.tsx +16 -0
  21. package/src/components/badge/badge.context.tsx +64 -0
  22. package/src/components/badge/badge.tsx +192 -0
  23. package/src/components/badge/badge.types.ts +10 -0
  24. package/src/components/badge/badge.variants.test.ts +269 -0
  25. package/src/components/badge/badge.variants.ts +238 -0
  26. package/src/components/badge/index.ts +17 -0
  27. package/src/components/bottom-sheet/AGENTS.md +232 -0
  28. package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
  29. package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
  30. package/src/components/bottom-sheet/bottom-sheet-container.tsx +245 -0
  31. package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
  32. package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
  33. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +148 -0
  34. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
  35. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
  36. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
  37. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +95 -0
  38. package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
  39. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
  40. package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
  41. package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
  42. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +276 -0
  43. package/src/components/bottom-sheet/bottom-sheet.variants.ts +199 -0
  44. package/src/components/bottom-sheet/index.ts +40 -0
  45. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
  46. package/src/components/button/AGENTS.md +217 -0
  47. package/src/components/button/button-end-content.tsx +15 -0
  48. package/src/components/button/button-group-separator.tsx +37 -0
  49. package/src/components/button/button-group-text.tsx +101 -0
  50. package/src/components/button/button-group.tsx +158 -0
  51. package/src/components/button/button-label.tsx +24 -0
  52. package/src/components/button/button-start-content.tsx +15 -0
  53. package/src/components/button/button.context.tsx +187 -0
  54. package/src/components/button/button.tsx +273 -0
  55. package/src/components/button/button.types.ts +10 -0
  56. package/src/components/button/button.variants.test.ts +811 -0
  57. package/src/components/button/button.variants.ts +480 -0
  58. package/src/components/button/index.ts +50 -0
  59. package/src/components/chart/AGENTS.md +210 -0
  60. package/src/components/chart/chart-area.tsx +93 -0
  61. package/src/components/chart/chart-bar.tsx +103 -0
  62. package/src/components/chart/chart-bars.tsx +75 -0
  63. package/src/components/chart/chart-candlestick.tsx +60 -0
  64. package/src/components/chart/chart-grid.tsx +33 -0
  65. package/src/components/chart/chart-legend.tsx +41 -0
  66. package/src/components/chart/chart-line.tsx +50 -0
  67. package/src/components/chart/chart-scatter.tsx +35 -0
  68. package/src/components/chart/chart-tooltip-dot.tsx +70 -0
  69. package/src/components/chart/chart-tooltip-x.tsx +121 -0
  70. package/src/components/chart/chart-tooltip-y.tsx +55 -0
  71. package/src/components/chart/chart-tooltip.tsx +133 -0
  72. package/src/components/chart/chart-x-axis.tsx +27 -0
  73. package/src/components/chart/chart-y-axis.tsx +21 -0
  74. package/src/components/chart/chart.context.tsx +127 -0
  75. package/src/components/chart/chart.tsx +448 -0
  76. package/src/components/chart/chart.types.ts +108 -0
  77. package/src/components/chart/chart.variants.test.ts +893 -0
  78. package/src/components/chart/chart.variants.ts +726 -0
  79. package/src/components/chart/index.ts +103 -0
  80. package/src/components/chart/pie-chart-center.tsx +40 -0
  81. package/src/components/chart/pie-chart-label.tsx +56 -0
  82. package/src/components/chart/pie-chart-slice.tsx +45 -0
  83. package/src/components/chart/pie-chart-tooltip.tsx +85 -0
  84. package/src/components/chart/pie-chart.context.tsx +77 -0
  85. package/src/components/chart/pie-chart.tsx +247 -0
  86. package/src/components/chart/use-chart-font.ts +25 -0
  87. package/src/components/chart/use-chart-palette.ts +38 -0
  88. package/src/components/checkbox/AGENTS.md +185 -0
  89. package/src/components/checkbox/checkbox-box.tsx +152 -0
  90. package/src/components/checkbox/checkbox-group.tsx +107 -0
  91. package/src/components/checkbox/checkbox-label.tsx +34 -0
  92. package/src/components/checkbox/checkbox.context.tsx +136 -0
  93. package/src/components/checkbox/checkbox.tsx +258 -0
  94. package/src/components/checkbox/checkbox.types.ts +14 -0
  95. package/src/components/checkbox/checkbox.variants.test.ts +634 -0
  96. package/src/components/checkbox/checkbox.variants.ts +484 -0
  97. package/src/components/checkbox/index.ts +42 -0
  98. package/src/components/field/AGENTS.md +100 -0
  99. package/src/components/field/field-content.tsx +24 -0
  100. package/src/components/field/field-description.tsx +29 -0
  101. package/src/components/field/field-error.tsx +61 -0
  102. package/src/components/field/field-group.tsx +22 -0
  103. package/src/components/field/field-label.tsx +33 -0
  104. package/src/components/field/field-legend.tsx +34 -0
  105. package/src/components/field/field-separator.tsx +81 -0
  106. package/src/components/field/field-set.tsx +22 -0
  107. package/src/components/field/field.context.tsx +78 -0
  108. package/src/components/field/field.tsx +144 -0
  109. package/src/components/field/field.types.ts +15 -0
  110. package/src/components/field/field.variants.test.ts +216 -0
  111. package/src/components/field/field.variants.ts +136 -0
  112. package/src/components/field/index.ts +21 -0
  113. package/src/components/icon/AGENTS.md +104 -0
  114. package/src/components/icon/icon.context.tsx +29 -0
  115. package/src/components/icon/icon.tsx +92 -0
  116. package/src/components/icon/icon.variants.test.ts +111 -0
  117. package/src/components/icon/icon.variants.ts +79 -0
  118. package/src/components/icon/index.ts +11 -0
  119. package/src/components/input/AGENTS.md +126 -0
  120. package/src/components/input/index.ts +27 -0
  121. package/src/components/input/input-group-decorator.tsx +84 -0
  122. package/src/components/input/input-group-prefix.tsx +16 -0
  123. package/src/components/input/input-group-suffix.tsx +15 -0
  124. package/src/components/input/input-group.tsx +140 -0
  125. package/src/components/input/input.context.tsx +87 -0
  126. package/src/components/input/input.tsx +184 -0
  127. package/src/components/input/input.types.ts +10 -0
  128. package/src/components/input/input.variants.test.ts +369 -0
  129. package/src/components/input/input.variants.ts +329 -0
  130. package/src/components/list-group/AGENTS.md +67 -0
  131. package/src/components/list-group/index.ts +20 -0
  132. package/src/components/list-group/list-group-item-content.tsx +10 -0
  133. package/src/components/list-group/list-group-item-description.tsx +12 -0
  134. package/src/components/list-group/list-group-item-prefix.tsx +30 -0
  135. package/src/components/list-group/list-group-item-suffix.tsx +55 -0
  136. package/src/components/list-group/list-group-item-title.tsx +17 -0
  137. package/src/components/list-group/list-group-item.tsx +87 -0
  138. package/src/components/list-group/list-group.context.tsx +65 -0
  139. package/src/components/list-group/list-group.tsx +116 -0
  140. package/src/components/list-group/list-group.types.ts +14 -0
  141. package/src/components/list-group/list-group.variants.test.ts +223 -0
  142. package/src/components/list-group/list-group.variants.ts +107 -0
  143. package/src/components/pressable/AGENTS.md +64 -0
  144. package/src/components/pressable/index.ts +9 -0
  145. package/src/components/pressable/pressable.tsx +256 -0
  146. package/src/components/pressable/pressable.variants.test.ts +99 -0
  147. package/src/components/pressable/pressable.variants.ts +63 -0
  148. package/src/components/provider/AGENTS.md +90 -0
  149. package/src/components/provider/index.ts +1 -0
  150. package/src/components/provider/provider.tsx +105 -0
  151. package/src/components/radio/AGENTS.md +247 -0
  152. package/src/components/radio/index.ts +35 -0
  153. package/src/components/radio/radio-group.tsx +126 -0
  154. package/src/components/radio/radio-indicator.tsx +91 -0
  155. package/src/components/radio/radio-label.tsx +39 -0
  156. package/src/components/radio/radio.context.tsx +132 -0
  157. package/src/components/radio/radio.tsx +215 -0
  158. package/src/components/radio/radio.variants.test.ts +581 -0
  159. package/src/components/radio/radio.variants.ts +272 -0
  160. package/src/components/screen/AGENTS.md +287 -0
  161. package/src/components/screen/index.ts +85 -0
  162. package/src/components/screen/screen-chat-list.tsx +485 -0
  163. package/src/components/screen/screen-content.tsx +69 -0
  164. package/src/components/screen/screen-debug.ts +43 -0
  165. package/src/components/screen/screen-error.tsx +61 -0
  166. package/src/components/screen/screen-flat-list.tsx +88 -0
  167. package/src/components/screen/screen-footer-background.tsx +60 -0
  168. package/src/components/screen/screen-footer.tsx +170 -0
  169. package/src/components/screen/screen-header.tsx +36 -0
  170. package/src/components/screen/screen-legend-list.tsx +105 -0
  171. package/src/components/screen/screen-list-component.tsx +23 -0
  172. package/src/components/screen/screen-loading.tsx +50 -0
  173. package/src/components/screen/screen-navbar-back-button.tsx +79 -0
  174. package/src/components/screen/screen-navbar-background.tsx +45 -0
  175. package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
  176. package/src/components/screen/screen-navbar-title.tsx +31 -0
  177. package/src/components/screen/screen-navbar.tsx +154 -0
  178. package/src/components/screen/screen-root.tsx +38 -0
  179. package/src/components/screen/screen-scroll-area.tsx +116 -0
  180. package/src/components/screen/screen-scroll-shadow.tsx +186 -0
  181. package/src/components/screen/screen-section-list.tsx +81 -0
  182. package/src/components/screen/screen-view.tsx +59 -0
  183. package/src/components/screen/screen.context.tsx +167 -0
  184. package/src/components/screen/screen.tsx +114 -0
  185. package/src/components/screen/screen.types.ts +66 -0
  186. package/src/components/screen/screen.variants.test.ts +602 -0
  187. package/src/components/screen/screen.variants.ts +475 -0
  188. package/src/components/screen/use-screen-scroll-insets.ts +218 -0
  189. package/src/components/separator/AGENTS.md +30 -0
  190. package/src/components/separator/index.ts +8 -0
  191. package/src/components/separator/separator.tsx +85 -0
  192. package/src/components/slider/AGENTS.md +274 -0
  193. package/src/components/slider/index.ts +48 -0
  194. package/src/components/slider/slider-fill.tsx +68 -0
  195. package/src/components/slider/slider-output.tsx +55 -0
  196. package/src/components/slider/slider-thumb.tsx +161 -0
  197. package/src/components/slider/slider-track.tsx +233 -0
  198. package/src/components/slider/slider.context.tsx +118 -0
  199. package/src/components/slider/slider.tsx +321 -0
  200. package/src/components/slider/slider.types.ts +26 -0
  201. package/src/components/slider/slider.variants.test.ts +804 -0
  202. package/src/components/slider/slider.variants.ts +607 -0
  203. package/src/components/spinner/AGENTS.md +73 -0
  204. package/src/components/spinner/index.ts +17 -0
  205. package/src/components/spinner/spinner-arc.tsx +90 -0
  206. package/src/components/spinner/spinner-content.tsx +63 -0
  207. package/src/components/spinner/spinner.context.tsx +52 -0
  208. package/src/components/spinner/spinner.tsx +128 -0
  209. package/src/components/spinner/spinner.variants.test.ts +273 -0
  210. package/src/components/spinner/spinner.variants.ts +187 -0
  211. package/src/components/switch/AGENTS.md +213 -0
  212. package/src/components/switch/index.ts +42 -0
  213. package/src/components/switch/switch-content.tsx +111 -0
  214. package/src/components/switch/switch-end-content.tsx +18 -0
  215. package/src/components/switch/switch-start-content.tsx +20 -0
  216. package/src/components/switch/switch-thumb.tsx +102 -0
  217. package/src/components/switch/switch.context.tsx +79 -0
  218. package/src/components/switch/switch.tsx +423 -0
  219. package/src/components/switch/switch.types.ts +14 -0
  220. package/src/components/switch/switch.variants.test.ts +570 -0
  221. package/src/components/switch/switch.variants.ts +511 -0
  222. package/src/components/tabs/AGENTS.md +286 -0
  223. package/src/components/tabs/index.ts +69 -0
  224. package/src/components/tabs/tabs-content.tsx +65 -0
  225. package/src/components/tabs/tabs-indicator.tsx +97 -0
  226. package/src/components/tabs/tabs-label.tsx +62 -0
  227. package/src/components/tabs/tabs-list.tsx +139 -0
  228. package/src/components/tabs/tabs-pager.tsx +59 -0
  229. package/src/components/tabs/tabs-scroll-view.tsx +171 -0
  230. package/src/components/tabs/tabs-separator.tsx +73 -0
  231. package/src/components/tabs/tabs-trigger.tsx +210 -0
  232. package/src/components/tabs/tabs.context.tsx +294 -0
  233. package/src/components/tabs/tabs.tsx +435 -0
  234. package/src/components/tabs/tabs.types.ts +13 -0
  235. package/src/components/tabs/tabs.variants.test.ts +1020 -0
  236. package/src/components/tabs/tabs.variants.ts +672 -0
  237. package/src/components/text/AGENTS.md +95 -0
  238. package/src/components/text/index.ts +25 -0
  239. package/src/components/text/text.context.tsx +60 -0
  240. package/src/components/text/text.tsx +251 -0
  241. package/src/components/text/text.variants.test.ts +422 -0
  242. package/src/components/text/text.variants.ts +282 -0
  243. package/src/display-name.test.ts +145 -0
  244. package/src/docs.test.ts +98 -0
  245. package/src/expo/navigation-theme.tsx +58 -0
  246. package/src/hooks/use-controllable-state.ts +45 -0
  247. package/src/hooks/use-keyboard-state-sync.tsx +147 -0
  248. package/src/hooks/use-navigation-theme.ts +78 -0
  249. package/src/hooks/use-theme-color.ts +44 -0
  250. package/src/icons/central.ts +1 -0
  251. package/src/lib/cn.test.ts +136 -0
  252. package/src/lib/cn.ts +29 -0
  253. package/src/lib/color.test.ts +80 -0
  254. package/src/lib/color.ts +79 -0
  255. package/src/lib/compose-refs.test.ts +64 -0
  256. package/src/lib/compose-refs.ts +37 -0
  257. package/src/lib/keyboard-animation.test.ts +33 -0
  258. package/src/lib/keyboard-animation.ts +28 -0
  259. package/src/lib/merge-props.test.ts +82 -0
  260. package/src/lib/merge-props.ts +47 -0
  261. package/src/lib/navigation-theme.test.ts +49 -0
  262. package/src/lib/navigation-theme.ts +53 -0
  263. package/src/lib/slot.tsx +45 -0
  264. package/src/lib/tv.ts +20 -0
  265. package/src/styles/base.css +2 -0
  266. package/src/styles/geometry.test.ts +117 -0
  267. package/src/styles/index.css +3 -0
  268. package/src/styles/theme-tokens.test.ts +252 -0
  269. package/src/styles/theme.css +447 -0
  270. package/src/styles/tokens.css +132 -0
  271. package/src/styles/tokens.test.ts +181 -0
  272. package/src/styles/tokens.ts +86 -0
  273. package/src/uniwind-env.d.ts +1 -0
@@ -0,0 +1,104 @@
1
+ import { type ReactElement, type ReactNode, useMemo } from "react";
2
+ import type { ViewProps } from "react-native";
3
+ import Animated, { interpolate, useAnimatedStyle } from "react-native-reanimated";
4
+ import { IconChevronBottom } from "../../icons/central";
5
+ import { Icon, type IconDefaults, IconDefaultsProvider } from "../icon";
6
+ import { useAccordionItemPart, useAccordionPart } from "./accordion.context";
7
+ import { ACCORDION_INDICATOR_ROTATION, ACCORDION_INDICATOR_TOKEN, accordionVariants } from "./accordion.variants";
8
+
9
+ /** What a render-function indicator is handed. */
10
+ export type AccordionIndicatorState = { isExpanded: boolean };
11
+
12
+ export type AccordionIndicatorProps = Omit<ViewProps, "children" | "style"> & {
13
+ /**
14
+ * Turn with the panel. Set `false` for a glyph that *swaps* rather than
15
+ * rotates — a plus becoming a minus reads as broken when it also spins.
16
+ */
17
+ isAnimated?: boolean;
18
+ className?: string;
19
+ /**
20
+ * A glyph, or a function handed the item's settled state.
21
+ *
22
+ * The function form exists so a swapping glyph needs no component of its own:
23
+ * reading `isExpanded` means calling a hook, and a hook cannot be called in the
24
+ * parent's JSX. `Slider.Track` takes a function for the same reason.
25
+ *
26
+ * It matters more than convenience here. A trigger finds its indicator by
27
+ * element type, so an indicator wrapped in a component of the caller's own is
28
+ * invisible to it and a second, default one is composed in beside it. Handing
29
+ * the state over removes the reason to write that wrapper at all.
30
+ */
31
+ children?: ReactNode | ((state: AccordionIndicatorState) => ReactNode);
32
+ };
33
+
34
+ /**
35
+ * The glyph saying an item opens, turning as it does.
36
+ *
37
+ * Composed into a trigger that holds none, so a bare `<Accordion.Trigger>` is
38
+ * already complete — `Radio`'s rule for its indicator, and the same reasoning: an
39
+ * item has exactly one, so a caller who wrote none wants the default.
40
+ *
41
+ * **The rotation interpolates off the item's own `progress`**, the very value the
42
+ * panel's height reads, so the glyph and the panel are in phase by construction
43
+ * rather than by two springs tuned to match. It also means a custom glyph turns
44
+ * exactly like the default one — pass anything as children and it rotates, unless
45
+ * `isAnimated={false}` says otherwise.
46
+ *
47
+ * The rotation lives on this node rather than on the trigger's because
48
+ * `Pressable`'s own root `Animated.View` already owns `transform` through a
49
+ * `useAnimatedStyle` of its own, and two animated styles on one node fight for
50
+ * the same prop.
51
+ *
52
+ * Its subtree inherits the accordion's glyph step and the muted token, so a
53
+ * composed `<Icon>` needs nothing said at the call site.
54
+ *
55
+ * @example
56
+ * <Accordion.Indicator isAnimated={false}>
57
+ * {({ isExpanded }) => <Icon icon={isExpanded ? IconMinusSmall : IconPlusSmall} />}
58
+ * </Accordion.Indicator>
59
+ */
60
+ export function AccordionIndicator({
61
+ isAnimated = true,
62
+ className,
63
+ children,
64
+ ...props
65
+ }: AccordionIndicatorProps): ReactElement {
66
+ const { size } = useAccordionPart("Accordion.Indicator");
67
+ const { isExpanded, progress } = useAccordionItemPart("Accordion.Indicator");
68
+
69
+ const slots = accordionVariants({ size });
70
+ const glyphClassName = slots.glyph();
71
+
72
+ const rotationStyle = useAnimatedStyle(() => ({
73
+ transform: [
74
+ {
75
+ rotate: `${interpolate(
76
+ progress.value,
77
+ [0, 1],
78
+ [ACCORDION_INDICATOR_ROTATION.collapsed, ACCORDION_INDICATOR_ROTATION.expanded]
79
+ )}deg`,
80
+ },
81
+ ],
82
+ }));
83
+
84
+ const iconDefaults = useMemo<IconDefaults>(
85
+ () => ({ className: glyphClassName, color: ACCORDION_INDICATOR_TOKEN }),
86
+ [glyphClassName]
87
+ );
88
+
89
+ return (
90
+ <Animated.View className={slots.indicator({ className })} style={isAnimated ? rotationStyle : undefined} {...props}>
91
+ <IconDefaultsProvider value={iconDefaults}>{renderGlyph(children, isExpanded)}</IconDefaultsProvider>
92
+ </Animated.View>
93
+ );
94
+ }
95
+ AccordionIndicator.displayName = "DelacourUI.Accordion.Indicator";
96
+
97
+ /** The caller's glyph, the state they asked for, or the default chevron. */
98
+ function renderGlyph(
99
+ children: ReactNode | ((state: AccordionIndicatorState) => ReactNode),
100
+ isExpanded: boolean
101
+ ): ReactNode {
102
+ if (typeof children === "function") return children({ isExpanded });
103
+ return children ?? <Icon icon={IconChevronBottom} />;
104
+ }
@@ -0,0 +1,78 @@
1
+ import { type ReactElement, type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { cancelAnimation, useSharedValue, withSpring } from "react-native-reanimated";
4
+ import { type AccordionItemContextValue, AccordionItemProvider, useAccordionPart } from "./accordion.context";
5
+ import {
6
+ ACCORDION_SPRING,
7
+ ACCORDION_UNMEASURED,
8
+ accordionVariants,
9
+ isItemExpanded,
10
+ resolveAccordionItemAxes,
11
+ } from "./accordion.variants";
12
+
13
+ export type AccordionItemProps = Omit<ViewProps, "children"> & {
14
+ /** What this item is called in the accordion's value. Must be unique within one. */
15
+ value: string;
16
+ /** Blocks the trigger and fades the row. Inherited from the enclosing accordion. */
17
+ isDisabled?: boolean;
18
+ className?: string;
19
+ children?: ReactNode;
20
+ };
21
+
22
+ /**
23
+ * One section of an accordion: a trigger, and the panel it discloses.
24
+ *
25
+ * Owns the two shared values every animated style on this item reads —
26
+ * `progress` and the panel's measured height — and the one spring that drives
27
+ * them. One source for the height, the fade and the indicator's rotation, so the
28
+ * three cannot drift out of step by a frame.
29
+ *
30
+ * `progress` is seeded from the settled state rather than from zero, so an item
31
+ * mounted open does not animate itself open on its first paint — `Switch`'s rule.
32
+ *
33
+ * An item is expected to hold an `Accordion.Content`. One without a panel has
34
+ * nothing to measure and therefore nothing to travel against, so its indicator
35
+ * stays put — which is the right outcome for a row that discloses nothing.
36
+ */
37
+ export function AccordionItem({ value, isDisabled, className, children, ...props }: AccordionItemProps): ReactElement {
38
+ const { expanded, isDisabled: accordionDisabled, size } = useAccordionPart("Accordion.Item");
39
+ const axes = resolveAccordionItemAxes({ own: { isDisabled }, root: { isDisabled: accordionDisabled } });
40
+ const isExpanded = isItemExpanded(expanded, value);
41
+
42
+ const progress = useSharedValue(isExpanded ? 1 : 0);
43
+ const contentHeight = useSharedValue(ACCORDION_UNMEASURED);
44
+
45
+ // Bumped the first time this item's panel reports a height, purely to make the
46
+ // effect below run again. See `onMeasured` on the item context for why the
47
+ // panel must not start the spring itself.
48
+ const [measurements, setMeasurements] = useState(0);
49
+ const onMeasured = useCallback(() => setMeasurements((count) => count + 1), []);
50
+
51
+ // `measurements` is not read in the body — being unread is the whole point of
52
+ // it, the way `settledDrags` is for a `Switch`. The first expand of a panel
53
+ // that has never been mounted has no height to travel against, so this run
54
+ // bails; the measurement that follows is what brings it back.
55
+ // biome-ignore lint/correctness/useExhaustiveDependencies: the extra dependency is the re-run trigger, see above
56
+ useEffect(() => {
57
+ if (isExpanded && contentHeight.value === ACCORDION_UNMEASURED) return;
58
+
59
+ progress.value = withSpring(isExpanded ? 1 : 0, ACCORDION_SPRING);
60
+
61
+ // Without this an item unmounted mid-travel leaves its spring running.
62
+ return () => cancelAnimation(progress);
63
+ }, [contentHeight, isExpanded, measurements, progress]);
64
+
65
+ const context = useMemo<AccordionItemContextValue>(
66
+ () => ({ contentHeight, isDisabled: axes.isDisabled, isExpanded, onMeasured, progress, value }),
67
+ [axes.isDisabled, contentHeight, isExpanded, onMeasured, progress, value]
68
+ );
69
+
70
+ return (
71
+ <AccordionItemProvider value={context}>
72
+ <View className={accordionVariants({ isDisabled: axes.isDisabled, size }).item({ className })} {...props}>
73
+ {children}
74
+ </View>
75
+ </AccordionItemProvider>
76
+ );
77
+ }
78
+ AccordionItem.displayName = "DelacourUI.Accordion.Item";
@@ -0,0 +1,18 @@
1
+ import type { ReactElement } from "react";
2
+ import { Text } from "../text";
3
+ import { useAccordionPart } from "./accordion.context";
4
+ import type { AccordionTextProps } from "./accordion.types";
5
+ import { accordionVariants } from "./accordion.variants";
6
+
7
+ /**
8
+ * The trigger's primary line — what the item is called.
9
+ *
10
+ * Carries its own colour and type scale, read from the accordion's context: a
11
+ * React Native `View` does not cascade colour to a `Text` descendant, so the
12
+ * trigger cannot set it on their behalf.
13
+ */
14
+ export function AccordionTitle({ className, ...props }: AccordionTextProps): ReactElement {
15
+ const { size } = useAccordionPart("Accordion.Title");
16
+ return <Text className={accordionVariants({ size }).title({ className })} {...props} />;
17
+ }
18
+ AccordionTitle.displayName = "DelacourUI.Accordion.Title";
@@ -0,0 +1,148 @@
1
+ import { Children, isValidElement, type ReactElement, type ReactNode, useCallback, useMemo } from "react";
2
+ import { View } from "react-native";
3
+ import { type IconDefaults, IconDefaultsProvider } from "../icon";
4
+ import { Pressable, type PressableProps } from "../pressable";
5
+ import { useAccordionItemPart, useAccordionPart } from "./accordion.context";
6
+ import { ACCORDION_FOREGROUND_TOKEN, accordionVariants } from "./accordion.variants";
7
+ import { AccordionDescription } from "./accordion-description";
8
+ import { AccordionIndicator } from "./accordion-indicator";
9
+ import { AccordionTitle } from "./accordion-title";
10
+
11
+ export type AccordionTriggerProps = Omit<PressableProps, "children" | "disabled" | "onPress"> & {
12
+ className?: string;
13
+ children?: ReactNode;
14
+ };
15
+
16
+ /**
17
+ * The row that opens and closes an item.
18
+ *
19
+ * A `Pressable`, so `feedback`, `haptic`, `pressedScale` and the rest come from
20
+ * there rather than being restated here. Two defaults differ: `fade`, because a
21
+ * full-bleed row that scales reads as the whole card flexing rather than as one
22
+ * row responding — `ListGroup.Item`'s reason — and `haptic="selection"`, because
23
+ * an item is a state toggle and the panel landing is the confirmation, which is
24
+ * `Checkbox`'s and `Switch`'s. `haptic={false}` silences it.
25
+ *
26
+ * `onPress` is the one prop `Omit`ed rather than forwarded: the press **is** the
27
+ * toggle, and a side effect belongs on the accordion's `onValueChange`. The trade
28
+ * `Checkbox` already makes.
29
+ *
30
+ * It assembles its own row — titles and descriptions stack in a column, an
31
+ * indicator is moved to the end, and one is composed in when the children hold
32
+ * none — so the anatomy needs no wrapper part between a trigger and its text.
33
+ *
34
+ * `accessibilityState.expanded` is the only thing added to what `Pressable`
35
+ * already announces. There is deliberately no separate heading element wrapping
36
+ * it: state set in two places is state a screen reader reads out twice.
37
+ */
38
+ export function AccordionTrigger({
39
+ feedback = "fade",
40
+ haptic = "selection",
41
+ className,
42
+ children,
43
+ ...props
44
+ }: AccordionTriggerProps): ReactElement {
45
+ const { size, toggle } = useAccordionPart("Accordion.Trigger");
46
+ const { isDisabled, isExpanded, value } = useAccordionItemPart("Accordion.Trigger");
47
+
48
+ const slots = accordionVariants({ size });
49
+ const columnClassName = slots.triggerContent();
50
+ const glyphClassName = slots.glyph();
51
+
52
+ const handlePress = useCallback(() => toggle(value), [toggle, value]);
53
+
54
+ const content = useMemo(() => composeRow(children, columnClassName), [children, columnClassName]);
55
+
56
+ const iconDefaults = useMemo<IconDefaults>(
57
+ () => ({ className: glyphClassName, color: ACCORDION_FOREGROUND_TOKEN }),
58
+ [glyphClassName]
59
+ );
60
+
61
+ return (
62
+ <Pressable
63
+ accessibilityState={{ expanded: isExpanded }}
64
+ className={slots.trigger({ className })}
65
+ disabled={isDisabled}
66
+ feedback={feedback}
67
+ haptic={haptic}
68
+ onPress={handlePress}
69
+ {...props}
70
+ >
71
+ <IconDefaultsProvider value={iconDefaults}>{content}</IconDefaultsProvider>
72
+ </Pressable>
73
+ );
74
+ }
75
+ AccordionTrigger.displayName = "DelacourUI.Accordion.Trigger";
76
+
77
+ /**
78
+ * Arranges a trigger's children into the row the anatomy describes.
79
+ *
80
+ * Three things happen, and each has a call site it saves:
81
+ *
82
+ * - **Bare strings become a title.** React Native crashes on a raw string outside
83
+ * a `<Text>`, so `<Accordion.Trigger>Shipping</Accordion.Trigger>` would red-box.
84
+ * Consecutive strings collapse into one, because `Item {index}` is a single
85
+ * piece of text and wrapping the parts separately would stack them.
86
+ * - **Titles and descriptions stack in a column.** They are the only children that
87
+ * belong on top of each other; anything else — a leading `Icon`, a `Badge` — is
88
+ * left where it was written, as a sibling on the row. That is what makes a
89
+ * `Accordion.TriggerContent` part unnecessary rather than merely absent.
90
+ * - **The indicator goes last, and is composed in when there is none.** So a
91
+ * trigger reads as its content and still ends with the glyph that says it opens.
92
+ *
93
+ * Lives here rather than with the root: it is the trigger that arranges its own
94
+ * children, and importing it from `./accordion` would close a cycle. See AGENTS.md.
95
+ */
96
+ function composeRow(children: ReactNode, columnClassName: string): ReactNode {
97
+ const items = Children.toArray(children);
98
+ const row: ReactNode[] = [];
99
+ const trailing: ReactNode[] = [];
100
+ let column: ReactNode[] = [];
101
+ let run: (string | number)[] = [];
102
+ let hasIndicator = false;
103
+
104
+ const flushRun = () => {
105
+ if (run.length === 0) return;
106
+ column.push(<AccordionTitle key={`title-${column.length}`}>{run.join("")}</AccordionTitle>);
107
+ run = [];
108
+ };
109
+
110
+ const flushColumn = () => {
111
+ if (column.length === 0) return;
112
+ row.push(
113
+ <View className={columnClassName} key={`content-${row.length}`}>
114
+ {column}
115
+ </View>
116
+ );
117
+ column = [];
118
+ };
119
+
120
+ for (const child of items) {
121
+ if (typeof child === "string" || typeof child === "number") {
122
+ run.push(child);
123
+ continue;
124
+ }
125
+ flushRun();
126
+
127
+ if (isValidElement(child) && child.type === AccordionIndicator) {
128
+ hasIndicator = true;
129
+ trailing.push(child);
130
+ continue;
131
+ }
132
+
133
+ if (isValidElement(child) && (child.type === AccordionTitle || child.type === AccordionDescription)) {
134
+ column.push(child);
135
+ continue;
136
+ }
137
+
138
+ flushColumn();
139
+ row.push(child);
140
+ }
141
+
142
+ flushRun();
143
+ flushColumn();
144
+
145
+ if (!hasIndicator) trailing.push(<AccordionIndicator key="indicator" />);
146
+
147
+ return [...row, ...trailing];
148
+ }
@@ -0,0 +1,153 @@
1
+ import { createContext, type ReactElement, type ReactNode, use } from "react";
2
+ import type { SharedValue } from "react-native-reanimated";
3
+ import type { AccordionSize, AccordionVariant } from "./accordion.variants";
4
+
5
+ export type AccordionContextValue = {
6
+ variant: AccordionVariant;
7
+ size: AccordionSize;
8
+ /** Every item is inert. An item's own `isDisabled` still overrides it, either way. */
9
+ isDisabled: boolean;
10
+ /** Which items are open, on the **JS thread**. */
11
+ expanded: readonly string[];
12
+ /** Opens or closes one item. Stable for the accordion's lifetime. */
13
+ toggle: (value: string) => void;
14
+ };
15
+
16
+ export type AccordionItemContextValue = {
17
+ /** What this item is called in the accordion's expanded set. */
18
+ value: string;
19
+ isDisabled: boolean;
20
+ /** Whether this item is open, on the **JS thread** — what accessibility reads. */
21
+ isExpanded: boolean;
22
+ /**
23
+ * How far open this item is, on the **UI thread**: `0` closed, `1` open.
24
+ *
25
+ * The one value every animated style on this item reads — the panel's height,
26
+ * the panel's opacity and the indicator's rotation. One source rather than
27
+ * three, so they cannot drift out of step by a frame.
28
+ *
29
+ * It agrees with {@link AccordionItemContextValue.isExpanded} at rest and
30
+ * disagrees for the whole of a spring, which is what keeps the animation off
31
+ * the JS thread entirely.
32
+ */
33
+ progress: SharedValue<number>;
34
+ /**
35
+ * The panel's natural height in points, from its own `onLayout`.
36
+ *
37
+ * Holds {@link ACCORDION_UNMEASURED} until a panel has reported, which is not
38
+ * the same as a panel that measured zero: the item's spring waits on the first
39
+ * measurement rather than travelling against a height that is not there yet.
40
+ */
41
+ contentHeight: SharedValue<number>;
42
+ /**
43
+ * Told by the panel that it has measured itself for the first time.
44
+ *
45
+ * Internal machinery, and the reason the item is the **only** owner of the
46
+ * spring. The panel cannot start the travel itself: `onLayout` is dispatched
47
+ * from the native side and can land either side of React's effects, so a panel
48
+ * that started its own spring would sometimes have it cancelled a moment later
49
+ * by the cleanup of the effect it raced. This hands the item a reason to re-run
50
+ * instead — `Slider`'s and `Switch`'s `settledDrags`, for the same reason: a
51
+ * counter whose only job is to give an effect something to fire on.
52
+ */
53
+ onMeasured: () => void;
54
+ };
55
+
56
+ const AccordionContext = createContext<AccordionContextValue | null>(null);
57
+ const AccordionItemContext = createContext<AccordionItemContextValue | null>(null);
58
+
59
+ /**
60
+ * Supplies one accordion's axes and its selection to every item beneath it.
61
+ *
62
+ * Lives in its own module, importing nothing but React and types, so a part can
63
+ * read it without importing `./accordion`. That import would close a cycle, and
64
+ * Metro serves a partially initialised module for a cycle — leaving the context
65
+ * `undefined` at import time and red-boxing the app on a cold start.
66
+ */
67
+ export function AccordionProvider({
68
+ value,
69
+ children,
70
+ }: {
71
+ value: AccordionContextValue;
72
+ children: ReactNode;
73
+ }): ReactElement {
74
+ return <AccordionContext value={value}>{children}</AccordionContext>;
75
+ }
76
+ AccordionProvider.displayName = "DelacourUI.Accordion.Provider";
77
+
78
+ /** Supplies one item's own state and shared values to its trigger, indicator and panel. */
79
+ export function AccordionItemProvider({
80
+ value,
81
+ children,
82
+ }: {
83
+ value: AccordionItemContextValue;
84
+ children: ReactNode;
85
+ }): ReactElement {
86
+ return <AccordionItemContext value={value}>{children}</AccordionItemContext>;
87
+ }
88
+ AccordionItemProvider.displayName = "DelacourUI.Accordion.ItemProvider";
89
+
90
+ /** The enclosing accordion's state, or null outside an `<Accordion>`. */
91
+ export function useAccordionContext(): AccordionContextValue | null {
92
+ return use(AccordionContext);
93
+ }
94
+
95
+ /**
96
+ * Reads the enclosing accordion's axes and selection.
97
+ *
98
+ * Lets a custom child style itself to match, or drive the selection, without the
99
+ * accordion having to pass props down through every item. Throws outside an
100
+ * `<Accordion>` — use {@link useAccordionContext} where the enclosing accordion is
101
+ * optional.
102
+ */
103
+ export function useAccordion(): AccordionContextValue {
104
+ const context = useAccordionContext();
105
+ if (!context) {
106
+ throw new Error("useAccordion must be called inside an <Accordion>.");
107
+ }
108
+ return context;
109
+ }
110
+
111
+ /** The enclosing item's state, or null outside an `<Accordion.Item>`. */
112
+ export function useAccordionItemContext(): AccordionItemContextValue | null {
113
+ return use(AccordionItemContext);
114
+ }
115
+
116
+ /**
117
+ * Reads the enclosing item's state.
118
+ *
119
+ * This is what a custom indicator reaches for: `isExpanded` to swap a glyph, or
120
+ * `progress` to animate against exactly the travel the panel is running. Throws
121
+ * outside an `<Accordion.Item>`.
122
+ */
123
+ export function useAccordionItem(): AccordionItemContextValue {
124
+ const context = useAccordionItemContext();
125
+ if (!context) {
126
+ throw new Error("useAccordionItem must be called inside an <Accordion.Item>.");
127
+ }
128
+ return context;
129
+ }
130
+
131
+ /**
132
+ * The enclosing accordion's state, for a compound part that cannot work without one.
133
+ *
134
+ * Internal: deliberately not re-exported from `index.ts`. A caller outside the
135
+ * library wants {@link useAccordion}, whose error message names the hook rather
136
+ * than a part.
137
+ */
138
+ export function useAccordionPart(component: string): AccordionContextValue {
139
+ const context = useAccordionContext();
140
+ if (!context) {
141
+ throw new Error(`${component} must be rendered inside an <Accordion>.`);
142
+ }
143
+ return context;
144
+ }
145
+
146
+ /** The enclosing item's state, for a compound part that cannot work without one. */
147
+ export function useAccordionItemPart(component: string): AccordionItemContextValue {
148
+ const context = useAccordionItemContext();
149
+ if (!context) {
150
+ throw new Error(`${component} must be rendered inside an <Accordion.Item>.`);
151
+ }
152
+ return context;
153
+ }