@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,95 @@
1
+ # Text
2
+
3
+ The library's type scale, and the one component that reproduces React Native's
4
+ own text inheritance through classNames. Compound root plus twelve presets:
5
+ `Display`, `Title`, `Header`, `Subheader`, `Paragraph`, `Label`, `Caption`,
6
+ `Overline`, and the four inline ones — `Strong`, `Emphasis`, `Link`, `Code`.
7
+
8
+ `import { Text } from "@delacour/react-native-ui/text";`
9
+
10
+ ## Files
11
+
12
+ | File | What it holds |
13
+ | --- | --- |
14
+ | `index.ts` | → `@delacour/react-native-ui/text` |
15
+ | `text.tsx` | Root, all twelve presets, the `Object.assign` surface — the pure-preset carve-out to one file per part, see [Compound component layout](../../../AGENTS.md#compound-component-layout) |
16
+ | `text.context.tsx` | `TextClassProvider`, `useTextClass()` |
17
+ | `text.variants.ts` | Pure `tv()` + `resolveTextClass`, no RN imports |
18
+ | `text.variants.test.ts` | |
19
+
20
+ ## Design
21
+
22
+ - **Every `Text` publishes its resolved class to its subtree**, and merges the
23
+ one it inherited. Natively a nested `<Text>` inherits the parent's *style
24
+ object* and overrides only the keys it sets; Uniwind compiles each `className`
25
+ independently, so the parent's resolved class has to be threaded through and
26
+ beaten per-axis instead. `resolveTextClass` is that chain, weakest first:
27
+ `TEXT_BASE_CLASS`, the inherited class, this text's named axes, the caller's
28
+ `className`. It is the ladder `resolveIconSizeClass` already follows.
29
+ - **`textVariants` has no `defaultVariants`, and must not gain any.** An axis the
30
+ caller did not name has to emit *nothing* so it falls through to the enclosing
31
+ text's class — that fall-through is the whole feature. A default would emit
32
+ from inside the same call, ahead of the inherited class in the merge, and every
33
+ nested `Text` would snap back to it. `iconVariants` carries the same rule for
34
+ the same reason. The fallback lives in `TEXT_BASE_CLASS` instead.
35
+ - **`TEXT_BASE_CLASS` is applied unconditionally**, never guarded behind "am I
36
+ nested". Under true nesting it is a no-op — the parent's string already names
37
+ every axis, so tailwind-merge drops it. It only does work under a *partial*
38
+ provider, a [`Button`](../button/AGENTS.md) publishing nothing but `text-primary-foreground`, where
39
+ guarding it would leave a nested `Text` with a colour and no size at all,
40
+ collapsing to React Native's own 14pt default.
41
+ - **The correctness property is a fixpoint**: for every class this resolver can
42
+ produce, a nested `Text` with no props of its own resolves to *exactly* its
43
+ parent's string, at any depth. The tests assert it across the matrix. Anything
44
+ that breaks it makes inheritance lossy in a way only a screen would reveal.
45
+ - **A variant names type, never a line height.** tailwind-merge lists `leading`
46
+ among `font-size`'s conflicting groups, so a `text-lg` from the `size` axis
47
+ silently deletes a `leading-6` written beside a `text-base` — no error, just a
48
+ paragraph that loses its leading when someone resizes it. Tailwind v4's
49
+ `--text-*--line-height` companions survive `tokens.css`'s size-only overrides,
50
+ so every step already carries a paired leading. Prose needing its own gets a
51
+ `--text-paragraph` / `--text-paragraph--line-height` **pair**, registered in
52
+ `tokens.ts` — one utility carrying both.
53
+ - **`color` is page-level only — no `-foreground` family.** `X-foreground` means
54
+ "content drawn on an `X` surface" (rule 10), and mapping a surface variant to
55
+ its foreground is already each surface component's job
56
+ (`BUTTON_FOREGROUND_TOKEN`, `LIST_GROUP_FOREGROUND_TOKEN`). A second copy of
57
+ that map here could drift from it. Text on a coloured surface writes the
58
+ utility in a `className`.
59
+ - **`align` is `left`/`center`/`right`, never `start`/`end`.** React Native's
60
+ `textAlign` accepts `auto | left | right | center | justify` and nothing else,
61
+ so Tailwind v4's logical-property utilities resolve to a value RN rejects.
62
+ `transform: "none"` emits `normal-case` rather than an empty string, so it can
63
+ actually clear an `Overline`'s `uppercase`.
64
+ - **A nested `Text.Code` cannot be padded.** A nested `<Text>` is laid out by the
65
+ platform's text engine — an `NSAttributedString` run on iOS, a `Span` on
66
+ Android — and both ignore padding, margin and border radius on an inner
67
+ `<Text>`. Only the background survives. The compound variant applies the pill
68
+ treatment to the standalone case only; a code *block* is a `View` the caller
69
+ wraps around it.
70
+ - **Anything can publish into the cascade, not just a `Text`.** [`Button`](../button/AGENTS.md) wraps
71
+ its children in a `TextClassProvider` carrying the label's treatment beside the
72
+ `IconDefaultsProvider` it already renders, so a bare `<Text>` composed into one
73
+ needs nothing at the call site — the payoff [`Icon`](../icon/AGENTS.md) already had. Publish a
74
+ treatment only where **one** covers the whole subtree: a [`ListGroup`](../list-group/AGENTS.md) row
75
+ (title + description), a navbar (title + subtitle) and [`Screen.Error`](../screen/AGENTS.md) (title +
76
+ message) each carry two, and one provider cannot serve both, so their parts
77
+ keep per-part classes.
78
+ - **It renders `Animated.Text`.** Animated text styles work anywhere with no
79
+ opt-in, at the cost of a Reanimated wrapper per text node. If a long list ever
80
+ profiles badly, the escape hatch is to base the root on RN's `Text` and let app
81
+ code write `<Animated.Text className={useTextClass()} style={…} />` — the
82
+ cascade is on context, so that inherits correctly. Measure first.
83
+ - **OS font scaling is respected and capped.** `allowFontScaling` is left at
84
+ React Native's default; `maxFontSizeMultiplier` defaults to
85
+ `TEXT_MAX_FONT_SIZE_MULTIPLIER`. The cap exists for the fixed-height chrome —
86
+ `h-button-*` and `h-navbar-row` cannot grow, so an uncapped multiplier clips a
87
+ label rather than enlarging it. Both stay overridable per call site.
88
+ - **No `asChild`.** `Slot` throws on a non-element child and a `Text`'s child is
89
+ usually a string, so it would be unusable in the shape people would reach for.
90
+ There is also nothing to donate: `useTextClass()` hands the class over directly.
91
+ - **`TEXT_VARIANTS` is ordered — block presets first, largest first.** The tests
92
+ read that order to assert the type scale descends, so a new block role goes in
93
+ at its step rather than on the end; appended, it would pass a suite that is no
94
+ longer checking anything. `TEXT_INLINE_VARIANTS` is the tail of the same list
95
+ and carries no scale, so it has no such constraint.
@@ -0,0 +1,25 @@
1
+ export { Text, type TextPresetProps, type TextProps } from "./text";
2
+ export { TextClassProvider, useTextClass } from "./text.context";
3
+ export {
4
+ canNestText,
5
+ isInlineTextVariant,
6
+ resolveTextClass,
7
+ TEXT_ALIGNS,
8
+ TEXT_BASE_CLASS,
9
+ TEXT_COLORS,
10
+ TEXT_INLINE_VARIANTS,
11
+ TEXT_MAX_FONT_SIZE_MULTIPLIER,
12
+ TEXT_SIZES,
13
+ TEXT_TRANSFORMS,
14
+ TEXT_VARIANTS,
15
+ TEXT_WEIGHTS,
16
+ type TextAlign,
17
+ type TextColor,
18
+ type TextInlineVariant,
19
+ type TextSize,
20
+ type TextTransform,
21
+ type TextVariant,
22
+ type TextVariantProps,
23
+ type TextWeight,
24
+ textVariants,
25
+ } from "./text.variants";
@@ -0,0 +1,60 @@
1
+ import { createContext, type ReactElement, type ReactNode, use } from "react";
2
+
3
+ const TextClassContext = createContext<string | undefined>(undefined);
4
+
5
+ /**
6
+ * Supplies the classes a `Text` in this subtree inherits.
7
+ *
8
+ * Two things publish through it, and one context rather than two is the point.
9
+ * A `Text` publishes its own resolved class, which is what makes a nested
10
+ * `<Text>` behave the way React Native's native text inheritance does — the
11
+ * child adopts the parent's treatment and overrides only the axes it names. And
12
+ * any component can publish a class of its own, which is what lets a text style
13
+ * be *composed into* a component rather than passed as a prop: a bare `<Text>`
14
+ * inside a `<Button>` comes out at the button's label colour and type scale
15
+ * with nothing said at the call site.
16
+ *
17
+ * That is the same cascade `IconDefaultsProvider` gives icons, and deliberately
18
+ * the same *single* path. `Spinner` once read two contexts to recompute one
19
+ * value; see AGENTS.md rule 3.
20
+ *
21
+ * The value is a class string rather than an object because a `Text`'s colour
22
+ * *is* a class — the asymmetry that forces `IconDefaults` to carry a colour
23
+ * token alongside its className does not exist here. Its being a primitive is
24
+ * also why nothing has to memoise it: React compares context by value, so an
25
+ * unchanged string re-renders no consumer. That matters when every `Text` in
26
+ * the tree renders one of these.
27
+ *
28
+ * `undefined` resets the cascade — wrap a subtree that must not inherit an
29
+ * enclosing `Text`'s treatment in `<TextClassProvider value={undefined}>`.
30
+ *
31
+ * Lives in its own module, importing nothing but React, so a component in
32
+ * another folder can publish into it without closing a cycle. See AGENTS.md
33
+ * rule 3.
34
+ */
35
+ export function TextClassProvider({
36
+ value,
37
+ children,
38
+ }: {
39
+ value: string | undefined;
40
+ children: ReactNode;
41
+ }): ReactElement {
42
+ return <TextClassContext value={value}>{children}</TextClassContext>;
43
+ }
44
+ TextClassProvider.displayName = "DelacourUI.Text.ClassProvider";
45
+
46
+ /**
47
+ * The classes the nearest enclosing `Text` or `TextClassProvider` published, or
48
+ * `undefined` outside one.
49
+ *
50
+ * Public, so a component this library does not own can join the cascade — an
51
+ * animated label is `<Animated.Text className={useTextClass()} style={…} />`
52
+ * and inherits correctly with nothing else wired up.
53
+ *
54
+ * `undefined` rather than a `null` sentinel: there is no difference worth
55
+ * modelling between "no provider" and "a provider with nothing to say", and
56
+ * `cn` already treats `undefined` as an absent link in the chain.
57
+ */
58
+ export function useTextClass(): string | undefined {
59
+ return use(TextClassContext);
60
+ }
@@ -0,0 +1,251 @@
1
+ import type { ComponentProps, ReactElement } from "react";
2
+ import Animated from "react-native-reanimated";
3
+ import { TextClassProvider, useTextClass } from "./text.context";
4
+ import {
5
+ canNestText,
6
+ resolveTextClass,
7
+ TEXT_MAX_FONT_SIZE_MULTIPLIER,
8
+ type TextAlign,
9
+ type TextColor,
10
+ type TextSize,
11
+ type TextTransform,
12
+ type TextVariant,
13
+ type TextWeight,
14
+ } from "./text.variants";
15
+
16
+ export type TextProps = Omit<ComponentProps<typeof Animated.Text>, "className"> & {
17
+ /** The named role. Omit it and the text inherits, falling back to the base treatment. */
18
+ variant?: TextVariant;
19
+ /** A step on the type scale. Beats the variant's own size. */
20
+ size?: TextSize;
21
+ weight?: TextWeight;
22
+ /**
23
+ * A page-level text colour. For text drawn on a coloured surface — a button,
24
+ * a badge — use a `className`: that mapping belongs to the surface.
25
+ */
26
+ color?: TextColor;
27
+ align?: TextAlign;
28
+ transform?: TextTransform;
29
+ /**
30
+ * Merged last, so it beats every named axis and anything inherited.
31
+ *
32
+ * An arbitrary font size has to be written as a length — `text-[17px]` or
33
+ * `text-(length:--x)`. A bare `text-[var(--x)]` is ambiguous to
34
+ * tailwind-merge, which files it under colour, so it will not override the
35
+ * inherited size.
36
+ */
37
+ className?: string;
38
+ };
39
+
40
+ /** A preset's props: every axis except the role it already is. */
41
+ export type TextPresetProps = Omit<TextProps, "variant">;
42
+
43
+ function TextRoot({
44
+ align,
45
+ children,
46
+ className,
47
+ color,
48
+ maxFontSizeMultiplier = TEXT_MAX_FONT_SIZE_MULTIPLIER,
49
+ size,
50
+ transform,
51
+ variant,
52
+ weight,
53
+ ...props
54
+ }: TextProps): ReactElement {
55
+ const inherited = useTextClass();
56
+
57
+ const resolved = resolveTextClass({
58
+ align,
59
+ className,
60
+ color,
61
+ inherited,
62
+ isNested: inherited !== undefined,
63
+ size,
64
+ transform,
65
+ variant,
66
+ weight,
67
+ });
68
+
69
+ const element = (
70
+ <Animated.Text className={resolved} maxFontSizeMultiplier={maxFontSizeMultiplier} {...props}>
71
+ {children}
72
+ </Animated.Text>
73
+ );
74
+
75
+ // A string or number child has no descendant that could read the context, so
76
+ // the provider is skipped rather than rendered once per label. `Text` is the
77
+ // most-instantiated component in an app; this is the common case.
78
+ return canNestText(children) ? <TextClassProvider value={resolved}>{element}</TextClassProvider> : element;
79
+ }
80
+
81
+ /**
82
+ * A screen's one hero line — 30pt bold.
83
+ *
84
+ * Announced as a heading, so a screen reader's rotor stops on it rather than
85
+ * reading it as one more line of body copy.
86
+ */
87
+ function TextDisplay(props: TextPresetProps): ReactElement {
88
+ return <TextRoot accessibilityRole="header" variant="display" {...props} />;
89
+ }
90
+ TextDisplay.displayName = "DelacourUI.Text.Display";
91
+
92
+ /** A screen or card title — 24pt bold. Announced as a heading. */
93
+ function TextTitle(props: TextPresetProps): ReactElement {
94
+ return <TextRoot accessibilityRole="header" variant="title" {...props} />;
95
+ }
96
+ TextTitle.displayName = "DelacourUI.Text.Title";
97
+
98
+ /** A section heading — 20pt semibold. Announced as a heading. */
99
+ function TextHeader(props: TextPresetProps): ReactElement {
100
+ return <TextRoot accessibilityRole="header" variant="header" {...props} />;
101
+ }
102
+ TextHeader.displayName = "DelacourUI.Text.Header";
103
+
104
+ /**
105
+ * The secondary line under a title — 18pt on the muted token.
106
+ *
107
+ * Deliberately not a fourth heading level, and deliberately not announced as
108
+ * one: it restates the title rather than introducing a new section.
109
+ */
110
+ function TextSubheader(props: TextPresetProps): ReactElement {
111
+ return <TextRoot variant="subheader" {...props} />;
112
+ }
113
+ TextSubheader.displayName = "DelacourUI.Text.Subheader";
114
+
115
+ /**
116
+ * A block of prose — 16pt on the foreground token.
117
+ *
118
+ * The same treatment a bare `<Text>` already renders. It exists so a paragraph
119
+ * *reads* as one at the call site, the way `Button.Label` exists even though a
120
+ * bare string child already produces one.
121
+ */
122
+ function TextParagraph(props: TextPresetProps): ReactElement {
123
+ return <TextRoot variant="paragraph" {...props} />;
124
+ }
125
+ TextParagraph.displayName = "DelacourUI.Text.Paragraph";
126
+
127
+ /** A control label or compact UI string — 14pt medium. */
128
+ function TextLabel(props: TextPresetProps): ReactElement {
129
+ return <TextRoot variant="label" {...props} />;
130
+ }
131
+ TextLabel.displayName = "DelacourUI.Text.Label";
132
+
133
+ /** Supporting copy under a label or a title — 14pt on the muted token. */
134
+ function TextCaption(props: TextPresetProps): ReactElement {
135
+ return <TextRoot variant="caption" {...props} />;
136
+ }
137
+ TextCaption.displayName = "DelacourUI.Text.Caption";
138
+
139
+ /** An eyebrow above a section — 12pt semibold, uppercase, muted. */
140
+ function TextOverline(props: TextPresetProps): ReactElement {
141
+ return <TextRoot variant="overline" {...props} />;
142
+ }
143
+ TextOverline.displayName = "DelacourUI.Text.Overline";
144
+
145
+ /**
146
+ * Inline emphasis by weight.
147
+ *
148
+ * Emits `font-semibold` and nothing else, so mid-paragraph it comes out at the
149
+ * surrounding text's size and colour. Standing on its own it falls back to the
150
+ * base treatment — React Native's own default colour does not follow the theme.
151
+ */
152
+ function TextStrong(props: TextPresetProps): ReactElement {
153
+ return <TextRoot variant="strong" {...props} />;
154
+ }
155
+ TextStrong.displayName = "DelacourUI.Text.Strong";
156
+
157
+ /** Inline emphasis by slant. Inherits size, weight and colour from the text around it. */
158
+ function TextEmphasis(props: TextPresetProps): ReactElement {
159
+ return <TextRoot variant="emphasis" {...props} />;
160
+ }
161
+ TextEmphasis.displayName = "DelacourUI.Text.Emphasis";
162
+
163
+ /**
164
+ * A tappable run of text. `onPress` rides through from React Native's own
165
+ * `TextProps` and works on a nested `<Text>` on both platforms.
166
+ *
167
+ * Painted with `info`, not `primary`: `--primary` is the primary surface
168
+ * colour and sits within a shade of `foreground` in both themes.
169
+ */
170
+ function TextLink(props: TextPresetProps): ReactElement {
171
+ return <TextRoot accessibilityRole="link" variant="link" {...props} />;
172
+ }
173
+ TextLink.displayName = "DelacourUI.Text.Link";
174
+
175
+ /**
176
+ * Inline code, monospaced on a muted background.
177
+ *
178
+ * Padded and rounded only when it stands alone. A nested `<Text>` is laid out
179
+ * by the platform's text engine rather than by Yoga — an `NSAttributedString`
180
+ * run on iOS, a `Span` on Android — and both ignore padding, margin and border
181
+ * radius outright, so only the background survives. For a code *block*, wrap it:
182
+ *
183
+ * @example
184
+ * <View className="rounded-lg bg-muted p-3">
185
+ * <Text.Code className="bg-transparent" size="sm">bun run gen-exports</Text.Code>
186
+ * </View>
187
+ */
188
+ function TextCode(props: TextPresetProps): ReactElement {
189
+ return <TextRoot variant="code" {...props} />;
190
+ }
191
+ TextCode.displayName = "DelacourUI.Text.Code";
192
+
193
+ /**
194
+ * A piece of text, and the type scale the whole library draws from.
195
+ *
196
+ * Every `Text` publishes its own resolved classes to its subtree, so a nested
197
+ * `<Text>` inherits the treatment around it and overrides only the axes it
198
+ * names — which is what React Native does natively with a nested `<Text>`'s
199
+ * style, and what Uniwind breaks by compiling each `className` independently.
200
+ *
201
+ * Precedence, weakest first: the base treatment, the inherited class, this
202
+ * text's own named axes, then its `className`.
203
+ *
204
+ * Anything can publish into that cascade, not just a `Text` — `Button` wraps its
205
+ * children in a `TextClassProvider` carrying the label's treatment, so a bare
206
+ * `<Text>` composed into one needs nothing at the call site. The same idea as
207
+ * `IconDefaultsProvider`, and the same single path.
208
+ *
209
+ * OS font scaling is respected and capped at {@link TEXT_MAX_FONT_SIZE_MULTIPLIER};
210
+ * both `allowFontScaling` and `maxFontSizeMultiplier` remain overridable.
211
+ *
212
+ * @example
213
+ * <Text.Title>Total <Text color="muted">USD</Text></Text.Title>
214
+ *
215
+ * @example
216
+ * <Text.Paragraph>
217
+ * Run <Text.Code>bun test</Text.Code> before you <Text.Strong>commit</Text.Strong>.
218
+ * </Text.Paragraph>
219
+ *
220
+ * @example
221
+ * // No `asChild`: a Text's child is usually a string, which `Slot` cannot take
222
+ * // props. Read the cascade directly instead.
223
+ * <Animated.Text className={useTextClass()} style={fadeStyle} />
224
+ */
225
+ export const Text = Object.assign(TextRoot, {
226
+ /** A screen's one hero line — 30pt bold. Announced as a heading. */
227
+ Display: TextDisplay,
228
+ /** A screen or card title — 24pt bold. Announced as a heading. */
229
+ Title: TextTitle,
230
+ /** A section heading — 20pt semibold. Announced as a heading. */
231
+ Header: TextHeader,
232
+ /** The secondary line under a title — 18pt muted. Not a fourth heading level. */
233
+ Subheader: TextSubheader,
234
+ /** A block of prose — 16pt. The same treatment a bare `<Text>` renders. */
235
+ Paragraph: TextParagraph,
236
+ /** A control label or compact UI string — 14pt medium. */
237
+ Label: TextLabel,
238
+ /** Supporting copy — 14pt on the muted token. */
239
+ Caption: TextCaption,
240
+ /** An eyebrow above a section — 12pt semibold, uppercase, muted. */
241
+ Overline: TextOverline,
242
+ /** Inline emphasis by weight. Inherits size and colour from the text around it. */
243
+ Strong: TextStrong,
244
+ /** Inline emphasis by slant. Inherits size, weight and colour from the text around it. */
245
+ Emphasis: TextEmphasis,
246
+ /** A tappable run of text, on the `info` token. Takes `onPress`. */
247
+ Link: TextLink,
248
+ /** Inline code, monospaced. Padded only when it stands alone. */
249
+ Code: TextCode,
250
+ displayName: "DelacourUI.Text",
251
+ });