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

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 +193 -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 +434 -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 +242 -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 +246 -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 +150 -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 +105 -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 +293 -0
  43. package/src/components/bottom-sheet/bottom-sheet.variants.ts +220 -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 +112 -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 +47 -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 +95 -0
  108. package/src/components/field/field.tsx +157 -0
  109. package/src/components/field/field.types.ts +15 -0
  110. package/src/components/field/field.variants.test.ts +240 -0
  111. package/src/components/field/field.variants.ts +156 -0
  112. package/src/components/field/index.ts +22 -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 +74 -0
  144. package/src/components/pressable/index.ts +9 -0
  145. package/src/components/pressable/pressable.tsx +261 -0
  146. package/src/components/pressable/pressable.variants.test.ts +128 -0
  147. package/src/components/pressable/pressable.variants.ts +80 -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 +245 -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 +580 -0
  159. package/src/components/radio/radio.variants.ts +271 -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 +50 -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 +193 -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 +856 -0
  202. package/src/components/slider/slider.variants.ts +661 -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 +287 -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 +670 -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,42 @@
1
+ export { Switch, type SwitchProps } from "./switch";
2
+ export { type SwitchContextValue, SwitchProvider, useSwitch, useSwitchContext } from "./switch.context";
3
+ export type { SwitchContentProps } from "./switch.types";
4
+ export {
5
+ hasThumbChild,
6
+ resolveSwitchAxes,
7
+ resolveSwitchContentTreatment,
8
+ resolveSwitchRelease,
9
+ resolveSwitchThumbTokens,
10
+ resolveSwitchTrackTokens,
11
+ SWITCH_COLORS,
12
+ SWITCH_CONTENT_ICON_STEP,
13
+ SWITCH_CONTENT_REST_TEXT_CLASS,
14
+ SWITCH_CONTENT_REST_TOKEN,
15
+ SWITCH_CONTENT_TEXT_CLASS,
16
+ SWITCH_DEFAULT_COLOR,
17
+ SWITCH_DEFAULT_SIZE,
18
+ SWITCH_FLING_VELOCITY,
19
+ SWITCH_INVALID_CONTENT_TEXT_CLASS,
20
+ SWITCH_INVALID_THUMB_TOKEN,
21
+ SWITCH_INVALID_TRACK_TOKEN,
22
+ SWITCH_PRESS_ANIMATION,
23
+ SWITCH_PRESS_SPRING,
24
+ SWITCH_SIZES,
25
+ SWITCH_TAP_SLOP,
26
+ SWITCH_THUMB_INSET,
27
+ SWITCH_THUMB_REST_TOKEN,
28
+ SWITCH_THUMB_SPRING,
29
+ SWITCH_THUMB_TOKEN,
30
+ SWITCH_TRACK_REST_TOKEN,
31
+ SWITCH_TRACK_TOKEN,
32
+ type SwitchAxes,
33
+ type SwitchColor,
34
+ type SwitchContentPlacement,
35
+ type SwitchFieldAxes,
36
+ type SwitchOwnAxes,
37
+ type SwitchSize,
38
+ type SwitchVariantProps,
39
+ switchTravel,
40
+ switchVariants,
41
+ } from "./switch.variants";
42
+ export type { SwitchThumbProps } from "./switch-thumb";
@@ -0,0 +1,111 @@
1
+ import { Children, type ReactElement, type ReactNode, useMemo } from "react";
2
+ import Animated, { useAnimatedStyle } from "react-native-reanimated";
3
+ import { useThemeColor } from "../../hooks/use-theme-color";
4
+ import { IconDefaultsProvider } from "../icon";
5
+ import { Text } from "../text";
6
+ import { TextClassProvider } from "../text/text.context";
7
+ import { useSwitchPart } from "./switch.context";
8
+ import type { SwitchContentProps } from "./switch.types";
9
+ import { resolveSwitchContentTreatment, type SwitchContentPlacement, switchVariants } from "./switch.variants";
10
+
11
+ /**
12
+ * The shared implementation behind `Switch.StartContent` and `.EndContent`.
13
+ *
14
+ * The two are the same box at opposite ends of the track — which end, and which
15
+ * way their opacity runs, are the only differences — so they share this leaf
16
+ * rather than duplicating {@link wrapTextChildren} and an animated style into
17
+ * two part files. `Input.Group`'s two decorators make the same trade.
18
+ *
19
+ * **The crossfade is the component's, not the caller's.** `StartContent` sits at
20
+ * the leading edge, which the thumb vacates as the switch turns **on**, so it
21
+ * fades in with `progress`; `EndContent` sits at the trailing edge and fades out
22
+ * with it. That is what lets both be written once with no conditionals — the
23
+ * thumb reads as uncovering the other end rather than as sliding over content
24
+ * that was always there.
25
+ *
26
+ * The layer is exactly the thumb's footprint at its own end, so a glyph is
27
+ * centred on the space the knob will vacate rather than beside it.
28
+ *
29
+ * Not a part itself: it takes the caller-facing name so the error thrown outside
30
+ * a switch names `Switch.StartContent` rather than something private.
31
+ */
32
+ export function SwitchContent({
33
+ part,
34
+ placement,
35
+ className,
36
+ children,
37
+ ...props
38
+ }: SwitchContentProps & { part: string; placement: SwitchContentPlacement }): ReactElement {
39
+ const { color, size, isDisabled, isInvalid, progress } = useSwitchPart(part);
40
+ const slots = switchVariants({ isDisabled, size });
41
+
42
+ const treatment = resolveSwitchContentTreatment({ color, isInvalid, placement });
43
+ const glyphColor = useThemeColor(treatment.color);
44
+ const glyphClassName = slots.glyph();
45
+ const iconDefaults = useMemo(
46
+ () => ({ className: glyphClassName, color: glyphColor ?? "" }),
47
+ [glyphClassName, glyphColor]
48
+ );
49
+
50
+ const isStart = placement === "start";
51
+ const contentStyle = useAnimatedStyle(() => ({
52
+ opacity: isStart ? progress.value : 1 - progress.value,
53
+ }));
54
+
55
+ const textClassName = slots.contentText({ className: treatment.textClass });
56
+
57
+ return (
58
+ <Animated.View
59
+ className={isStart ? slots.startContent({ className }) : slots.endContent({ className })}
60
+ // Never the element a screen reader lands on. The switch announces itself
61
+ // as one control with a checked state, and a decorative glyph either side
62
+ // of the knob has nothing to add to that.
63
+ accessible={false}
64
+ importantForAccessibility="no-hide-descendants"
65
+ pointerEvents="none"
66
+ style={contentStyle}
67
+ {...props}
68
+ >
69
+ <IconDefaultsProvider value={iconDefaults}>
70
+ <TextClassProvider value={textClassName}>{wrapTextChildren(children)}</TextClassProvider>
71
+ </IconDefaultsProvider>
72
+ </Animated.View>
73
+ );
74
+ }
75
+ SwitchContent.displayName = "DelacourUI.Switch.Content";
76
+
77
+ /**
78
+ * Wraps bare text children in a `Text`.
79
+ *
80
+ * A content layer renders a `View`, and React Native cannot render a string
81
+ * outside a `<Text>` — so `<Switch.EndContent>OFF</Switch.EndContent>`, which is
82
+ * the shortest thing anyone will write, would otherwise crash. Consecutive
83
+ * strings and numbers collapse into one `Text` rather than one each, the same
84
+ * rule and the same reason as `Input`'s decorators.
85
+ *
86
+ * The wrapped text needs no className: the layer has already published its
87
+ * treatment through `TextClassProvider`.
88
+ */
89
+ function wrapTextChildren(children: ReactNode): ReactNode {
90
+ const items = Children.toArray(children);
91
+ const output: ReactNode[] = [];
92
+ let run: (string | number)[] = [];
93
+
94
+ const flushRun = () => {
95
+ if (run.length === 0) return;
96
+ output.push(<Text key={`content-${output.length}`}>{run.join("")}</Text>);
97
+ run = [];
98
+ };
99
+
100
+ for (const child of items) {
101
+ if (typeof child === "string" || typeof child === "number") {
102
+ run.push(child);
103
+ continue;
104
+ }
105
+ flushRun();
106
+ output.push(child);
107
+ }
108
+ flushRun();
109
+
110
+ return output;
111
+ }
@@ -0,0 +1,18 @@
1
+ import type { ReactElement } from "react";
2
+ import type { SwitchContentProps } from "./switch.types";
3
+ import { SwitchContent } from "./switch-content";
4
+
5
+ /**
6
+ * Content at the trailing edge of the track, behind the thumb.
7
+ *
8
+ * Revealed as the switch turns **off** — this is the end the knob vacates — so
9
+ * it is where the "off" mark goes: a cross, a moon, the word OFF. The fade is
10
+ * the component's, so write it once and unconditionally.
11
+ *
12
+ * It sits on the resting track rather than the coloured one, so its glyph takes
13
+ * a muted foreground rather than the switch's colour.
14
+ */
15
+ export function SwitchEndContent(props: SwitchContentProps): ReactElement {
16
+ return <SwitchContent part="Switch.EndContent" placement="end" {...props} />;
17
+ }
18
+ SwitchEndContent.displayName = "DelacourUI.Switch.EndContent";
@@ -0,0 +1,20 @@
1
+ import type { ReactElement } from "react";
2
+ import type { SwitchContentProps } from "./switch.types";
3
+ import { SwitchContent } from "./switch-content";
4
+
5
+ /**
6
+ * Content at the leading edge of the track, behind the thumb.
7
+ *
8
+ * Revealed as the switch turns **on** — this is the end the knob vacates — so it
9
+ * is where the "on" mark goes: a tick, a sun, the word ON. The fade is the
10
+ * component's, so write it once and unconditionally; there is no `isSelected &&`
11
+ * to remember.
12
+ *
13
+ * An `Icon` needs nothing said at the call site: it inherits the switch's glyph
14
+ * step and the foreground of the colour the track fades to. A bare string is
15
+ * wrapped in a `Text` that inherits the same treatment.
16
+ */
17
+ export function SwitchStartContent(props: SwitchContentProps): ReactElement {
18
+ return <SwitchContent part="Switch.StartContent" placement="start" {...props} />;
19
+ }
20
+ SwitchStartContent.displayName = "DelacourUI.Switch.StartContent";
@@ -0,0 +1,102 @@
1
+ import { type ReactElement, type ReactNode, useCallback, useMemo } from "react";
2
+ import type { LayoutChangeEvent, ViewProps } from "react-native";
3
+ import Animated, { interpolateColor, useAnimatedStyle } from "react-native-reanimated";
4
+ import { useThemeColor } from "../../hooks/use-theme-color";
5
+ import { IconDefaultsProvider } from "../icon";
6
+ import { useSwitchPart } from "./switch.context";
7
+ import {
8
+ resolveSwitchThumbTokens,
9
+ resolveSwitchTrackTokens,
10
+ SWITCH_THUMB_INSET,
11
+ switchTravel,
12
+ switchVariants,
13
+ } from "./switch.variants";
14
+
15
+ export type SwitchThumbProps = Omit<ViewProps, "children" | "style"> & {
16
+ className?: string;
17
+ /** Drawn inside the knob. An `Icon` needs nothing said at the call site. */
18
+ children?: ReactNode;
19
+ };
20
+
21
+ /**
22
+ * The knob that slides between the two ends of the track.
23
+ *
24
+ * Composed in automatically when the children hold none, so `<Switch />` is
25
+ * already a complete control — `Radio`'s rule for its indicator. Write it out by
26
+ * hand only to restyle it, or to put a glyph inside it.
27
+ *
28
+ * **It holds no gesture of its own.** One `Gesture.Pan()` on the root drives it,
29
+ * because a touch anywhere on the pill should move the knob it is about to
30
+ * move — and a gesture here would nest a descendant recogniser inside the root's,
31
+ * leaving two to negotiate for one drag. `Slider.Thumb` makes the same trade for
32
+ * the same reason.
33
+ *
34
+ * **One animated style, one node.** The position and the colour are entries in a
35
+ * single `useAnimatedStyle`, because two calls on one view fight for the same
36
+ * props and the later one silently wins — the rule `Radio.Indicator` states.
37
+ *
38
+ * **The colour is not a class.** It fades between two token *values* as the
39
+ * switch travels, and a colour being interpolated cannot be a class — the rule
40
+ * `Checkbox`'s border already follows. The `thumb` slot keeps `bg-background` as
41
+ * the resting appearance this style starts from, and nothing else names a
42
+ * background; two sources for one surface is how a class and a style end up
43
+ * disagreeing for a frame on every toggle.
44
+ *
45
+ * **It is invisible until both layouts have reported.** The travel is measured,
46
+ * not tabulated — `size-icon-xl` cannot be read from JavaScript — and a knob
47
+ * drawn before then sits at a garbage offset for a frame, which reads as a
48
+ * flicker on every mount.
49
+ *
50
+ * **It does not scale when grabbed, and the track does instead.** The track
51
+ * clips, so a scaled knob would be cut off by its own capsule — a bite taken out
52
+ * of the knob rather than an acknowledgement of the press. The outermost node is
53
+ * the one thing nothing can crop, so that is where the press feedback lives.
54
+ */
55
+ export function SwitchThumb({ className, children, ...props }: SwitchThumbProps): ReactElement {
56
+ const { color, size, isInvalid, isDisabled, progress, trackWidth, thumbWidth } = useSwitchPart("Switch.Thumb");
57
+
58
+ const handleLayout = useCallback(
59
+ (event: LayoutChangeEvent) => {
60
+ thumbWidth.value = event.nativeEvent.layout.width;
61
+ },
62
+ [thumbWidth]
63
+ );
64
+
65
+ const tokens = resolveSwitchThumbTokens({ color, isInvalid });
66
+ const restColor = useThemeColor(tokens.rest) ?? "transparent";
67
+ const activeColor = useThemeColor(tokens.active) ?? "transparent";
68
+
69
+ const thumbStyle = useAnimatedStyle(() => {
70
+ const measured = trackWidth.value;
71
+ const travel = switchTravel({
72
+ inset: SWITCH_THUMB_INSET,
73
+ thumbWidth: thumbWidth.value,
74
+ trackWidth: measured,
75
+ });
76
+
77
+ return {
78
+ backgroundColor: interpolateColor(progress.value, [0, 1], [restColor, activeColor]),
79
+ opacity: measured > 0 ? 1 : 0,
80
+ transform: [{ translateX: progress.value * travel }],
81
+ };
82
+ });
83
+
84
+ // A glyph on the knob takes the *track*'s colour, not the knob's own: it is
85
+ // drawn on the knob, so it has to contrast with it, and the one value that
86
+ // reads at both ends is the colour the track fades to. Static rather than
87
+ // interpolated because an `Icon`'s colour is a prop, not a style.
88
+ const slots = switchVariants({ isDisabled, size });
89
+ const glyphColor = useThemeColor(resolveSwitchTrackTokens({ color, isInvalid }).active);
90
+ const glyphClassName = slots.glyph();
91
+ const iconDefaults = useMemo(
92
+ () => ({ className: glyphClassName, color: glyphColor ?? "" }),
93
+ [glyphClassName, glyphColor]
94
+ );
95
+
96
+ return (
97
+ <Animated.View className={slots.thumb({ className })} onLayout={handleLayout} style={thumbStyle} {...props}>
98
+ {children ? <IconDefaultsProvider value={iconDefaults}>{children}</IconDefaultsProvider> : null}
99
+ </Animated.View>
100
+ );
101
+ }
102
+ SwitchThumb.displayName = "DelacourUI.Switch.Thumb";
@@ -0,0 +1,79 @@
1
+ import { createContext, type ReactElement, type ReactNode, use } from "react";
2
+ import type { SharedValue } from "react-native-reanimated";
3
+ import type { SwitchColor, SwitchSize } from "./switch.variants";
4
+
5
+ export type SwitchContextValue = {
6
+ color: SwitchColor;
7
+ size: SwitchSize;
8
+ /** Whether the switch is on, on the **JS thread** — what accessibility reads. */
9
+ isSelected: boolean;
10
+ isDisabled: boolean;
11
+ isInvalid: boolean;
12
+ /**
13
+ * Where the thumb is, on the **UI thread**: `0` at the leading edge, `1` at
14
+ * the trailing one.
15
+ *
16
+ * The one value every animated style on this control reads — the thumb's
17
+ * position and colour, the track's colour, and both content layers' opacity.
18
+ * One source rather than four, so they cannot drift out of step by a frame.
19
+ *
20
+ * It agrees with {@link SwitchContextValue.isSelected} at rest and disagrees
21
+ * for the whole of a drag, which is the entire reason the drag never waits on
22
+ * React.
23
+ */
24
+ progress: SharedValue<number>;
25
+ /** The track's own width, from its `onLayout`. `0` until it has been measured. */
26
+ trackWidth: SharedValue<number>;
27
+ /** The thumb's width, from its `onLayout`. `0` until it has been measured. */
28
+ thumbWidth: SharedValue<number>;
29
+ };
30
+
31
+ const SwitchContext = createContext<SwitchContextValue | null>(null);
32
+
33
+ /**
34
+ * Supplies one switch's settled state and shared values to its own parts.
35
+ *
36
+ * Lives in its own module, importing nothing but React and types, so a part can
37
+ * read it without importing `./switch`. That import would close a cycle, and
38
+ * Metro serves a partially initialised module for a cycle — leaving the context
39
+ * `undefined` at import time and red-boxing the app on a cold start.
40
+ */
41
+ export function SwitchProvider({ value, children }: { value: SwitchContextValue; children: ReactNode }): ReactElement {
42
+ return <SwitchContext value={value}>{children}</SwitchContext>;
43
+ }
44
+ SwitchProvider.displayName = "DelacourUI.Switch.Provider";
45
+
46
+ /** The enclosing switch's state, or null outside a `<Switch>`. */
47
+ export function useSwitchContext(): SwitchContextValue | null {
48
+ return use(SwitchContext);
49
+ }
50
+
51
+ /**
52
+ * Reads the enclosing switch's settled state.
53
+ *
54
+ * Lets a custom child style itself to match without the switch having to pass
55
+ * props down through every slot. Throws outside a `<Switch>` — use
56
+ * {@link useSwitchContext} where the enclosing switch is optional.
57
+ */
58
+ export function useSwitch(): SwitchContextValue {
59
+ const context = useSwitchContext();
60
+ if (!context) {
61
+ throw new Error("useSwitch must be called inside a <Switch>.");
62
+ }
63
+ return context;
64
+ }
65
+
66
+ /**
67
+ * The enclosing switch's state, for a compound part that cannot work without one.
68
+ *
69
+ * Internal: deliberately not re-exported from `index.ts`. A caller outside the
70
+ * library wants {@link useSwitch}, whose error message names the hook rather
71
+ * than a part.
72
+ */
73
+ export function useSwitchPart(component: string): SwitchContextValue {
74
+ const context = useSwitchContext();
75
+ if (!context) {
76
+ throw new Error(`${component} must be rendered inside a <Switch>.`);
77
+ }
78
+ return context;
79
+ }