@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,422 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { cn } from "../../lib/cn";
3
+ import {
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
+ textVariants,
17
+ } from "./text.variants";
18
+
19
+ /** Tailwind's type scale, smallest first — position, so a test can say "a step down". */
20
+ const TYPE_SCALE = ["text-xs", "text-sm", "text-base", "text-lg", "text-xl", "text-2xl", "text-3xl"];
21
+
22
+ /** The three alignment utilities, which share the `text-` prefix with sizes and colours. */
23
+ const ALIGNMENTS = ["text-left", "text-center", "text-right"];
24
+
25
+ /** The single font-size utility in a class string, or undefined. */
26
+ function fontSize(cls: string): string | undefined {
27
+ return cls.split(" ").find((name) => TYPE_SCALE.includes(name));
28
+ }
29
+
30
+ /** Position of a class string's font size on the type scale, or -1. */
31
+ function typeStep(cls: string): number {
32
+ const size = fontSize(cls);
33
+ return size ? TYPE_SCALE.indexOf(size) : -1;
34
+ }
35
+
36
+ /** The single font-weight utility in a class string, or undefined. */
37
+ function weightOf(cls: string): string | undefined {
38
+ return cls.split(" ").find((name) => /^font-(normal|medium|semibold|bold)$/.test(name));
39
+ }
40
+
41
+ /** The single text-colour utility — a `text-*` that is neither a size nor an alignment. */
42
+ function colorOf(cls: string): string | undefined {
43
+ return cls
44
+ .split(" ")
45
+ .find((name) => name.startsWith("text-") && !TYPE_SCALE.includes(name) && !ALIGNMENTS.includes(name));
46
+ }
47
+
48
+ /** Variants that carry a full spec, as opposed to the inline deltas. */
49
+ const BLOCK_VARIANTS = TEXT_VARIANTS.filter((variant) => !isInlineTextVariant(variant));
50
+
51
+ describe("textVariants", () => {
52
+ // The defaultVariants regression guard, and the reason this whole design
53
+ // works: an axis the caller does not name has to emit NOTHING so it falls
54
+ // through to the class the enclosing Text published. `iconVariants` carries
55
+ // the same test for the same reason.
56
+ test("emits nothing without props, so an inherited class survives", () => {
57
+ expect(textVariants() ?? "").toBe("");
58
+ });
59
+
60
+ test("gives every variant a distinct treatment", () => {
61
+ const seen = new Set(TEXT_VARIANTS.map((variant) => textVariants({ variant })));
62
+ expect(seen.size).toBe(TEXT_VARIANTS.length);
63
+ });
64
+
65
+ test("every block variant names a font size and a colour", () => {
66
+ for (const variant of BLOCK_VARIANTS) {
67
+ const cls = textVariants({ variant });
68
+ expect(fontSize(cls)).toBeDefined();
69
+ expect(colorOf(cls)).toBeDefined();
70
+ }
71
+ });
72
+
73
+ // The point of an inline preset: it emits a delta and nothing else, so a
74
+ // `Text.Strong` mid-paragraph comes out at the paragraph's size and colour
75
+ // rather than snapping back to a base of its own.
76
+ test("no inline variant names a font size or a colour", () => {
77
+ for (const variant of TEXT_INLINE_VARIANTS) {
78
+ const cls = textVariants({ variant });
79
+ expect(fontSize(cls)).toBeUndefined();
80
+ expect(typeStep(cls)).toBe(-1);
81
+ }
82
+ });
83
+
84
+ test("the block variants descend in type scale in the order the registry lists them", () => {
85
+ const steps = BLOCK_VARIANTS.map((variant) => typeStep(textVariants({ variant })));
86
+ expect(steps).not.toContain(-1);
87
+ expect([...steps]).toEqual([...steps].sort((a, b) => b - a));
88
+ });
89
+
90
+ test("gives every size a distinct step on the type scale", () => {
91
+ const steps = TEXT_SIZES.map((size) => typeStep(textVariants({ size })));
92
+ expect(steps).not.toContain(-1);
93
+ expect(new Set(steps).size).toBe(TEXT_SIZES.length);
94
+ expect([...steps]).toEqual([...steps].sort((a, b) => a - b));
95
+ });
96
+
97
+ test("no size emits a weight or a colour", () => {
98
+ for (const size of TEXT_SIZES) {
99
+ const cls = textVariants({ size });
100
+ expect(weightOf(cls)).toBeUndefined();
101
+ expect(colorOf(cls)).toBeUndefined();
102
+ }
103
+ });
104
+
105
+ test("gives every colour a distinct token, and no size or weight", () => {
106
+ const seen = new Set(TEXT_COLORS.map((color) => textVariants({ color })));
107
+ expect(seen.size).toBe(TEXT_COLORS.length);
108
+
109
+ for (const color of TEXT_COLORS) {
110
+ const cls = textVariants({ color });
111
+ expect(colorOf(cls)).toBeDefined();
112
+ expect(fontSize(cls)).toBeUndefined();
113
+ expect(weightOf(cls)).toBeUndefined();
114
+ }
115
+ });
116
+
117
+ // A bespoke `--text-*` token would land in tailwind-merge's COLOUR group, so
118
+ // a colour class and a size class would be judged to conflict and one would
119
+ // silently vanish. This is the guard that every colour here is really a colour.
120
+ test("every colour survives alongside a font size", () => {
121
+ for (const color of TEXT_COLORS) {
122
+ const merged = cn("text-lg", textVariants({ color }));
123
+ expect(fontSize(merged)).toBe("text-lg");
124
+ expect(colorOf(merged)).toBe(colorOf(textVariants({ color })));
125
+ }
126
+ });
127
+
128
+ test("gives every weight a distinct utility, and no size or colour", () => {
129
+ const seen = new Set(TEXT_WEIGHTS.map((weight) => textVariants({ weight })));
130
+ expect(seen.size).toBe(TEXT_WEIGHTS.length);
131
+
132
+ for (const weight of TEXT_WEIGHTS) {
133
+ const cls = textVariants({ weight });
134
+ expect(weightOf(cls)).toBeDefined();
135
+ expect(fontSize(cls)).toBeUndefined();
136
+ expect(colorOf(cls)).toBeUndefined();
137
+ }
138
+ });
139
+
140
+ // React Native's `textAlign` accepts auto | left | right | center | justify
141
+ // and nothing else, so Tailwind v4's logical `text-start` / `text-end` would
142
+ // resolve to a value RN rejects.
143
+ test("aligns with physical utilities, never logical ones", () => {
144
+ for (const align of TEXT_ALIGNS) {
145
+ const cls = textVariants({ align });
146
+ expect(ALIGNMENTS).toContain(cls);
147
+ }
148
+ expect(TEXT_ALIGNS).not.toContain("start");
149
+ expect(TEXT_ALIGNS).not.toContain("end");
150
+ });
151
+
152
+ test("gives every transform a distinct utility", () => {
153
+ const seen = new Set(TEXT_TRANSFORMS.map((transform) => textVariants({ transform })));
154
+ expect(seen.size).toBe(TEXT_TRANSFORMS.length);
155
+ });
156
+
157
+ // An empty string could not clear `overline`'s `uppercase`; `normal-case` can.
158
+ test("transform none emits normal-case so it can clear an inherited uppercase", () => {
159
+ expect(textVariants({ transform: "none" })).toBe("normal-case");
160
+ });
161
+
162
+ // tailwind-merge lists `leading` among `font-size`'s conflicting groups, so a
163
+ // `text-lg` from the size axis silently deletes a `leading-*` written beside a
164
+ // `text-*` in a variant string. Line height belongs in a
165
+ // `--text-<name>--line-height` companion instead. This asserts none crept in.
166
+ test("no variant writes a line height beside its font size", () => {
167
+ for (const variant of TEXT_VARIANTS) {
168
+ expect(textVariants({ variant })).not.toMatch(/\bleading-/);
169
+ }
170
+ });
171
+
172
+ test("a variant and a colour compose without either dropping the other", () => {
173
+ for (const variant of BLOCK_VARIANTS) {
174
+ for (const color of TEXT_COLORS) {
175
+ const cls = textVariants({ color, variant });
176
+ expect(fontSize(cls)).toBe(fontSize(textVariants({ variant })));
177
+ expect(colorOf(cls)).toBe(colorOf(textVariants({ color })));
178
+ }
179
+ }
180
+ });
181
+ });
182
+
183
+ describe("TEXT_BASE_CLASS", () => {
184
+ // `font-sans` is what makes the typeface themeable, and two presets have to
185
+ // beat it — `Text.Code` and the three headings. All three land in
186
+ // tailwind-merge's font-family group, so this only holds while the base
187
+ // stays ahead of the variants in resolveTextClass's chain.
188
+ test("gives up its family to Text.Code and to a heading", () => {
189
+ const code = resolveTextClass({ variant: "code" });
190
+ expect(code).toContain("font-mono");
191
+ expect(code).not.toContain("font-sans");
192
+
193
+ for (const variant of ["display", "title", "header"] as const) {
194
+ const resolved = resolveTextClass({ variant });
195
+ expect(resolved).toContain("font-heading");
196
+ expect(resolved).not.toContain("font-sans");
197
+ }
198
+ });
199
+
200
+ test("names a family, a size, a weight and a colour — a Text is never unstyled", () => {
201
+ expect(TEXT_BASE_CLASS).toContain("font-sans");
202
+ expect(fontSize(TEXT_BASE_CLASS)).toBeDefined();
203
+ expect(weightOf(TEXT_BASE_CLASS)).toBeDefined();
204
+ expect(colorOf(TEXT_BASE_CLASS)).toBe("text-foreground");
205
+ });
206
+
207
+ test("loses every axis it names to a class that names the same one", () => {
208
+ const resolved = resolveTextClass({ inherited: "font-mono font-bold text-2xl text-destructive" });
209
+ for (const name of TEXT_BASE_CLASS.split(" ")) {
210
+ expect(resolved).not.toContain(name);
211
+ }
212
+ });
213
+ });
214
+
215
+ describe("isInlineTextVariant", () => {
216
+ test("separates the deltas from the full specs", () => {
217
+ for (const variant of TEXT_INLINE_VARIANTS) {
218
+ expect(isInlineTextVariant(variant)).toBe(true);
219
+ }
220
+ expect(isInlineTextVariant("paragraph")).toBe(false);
221
+ expect(isInlineTextVariant("display")).toBe(false);
222
+ });
223
+
224
+ test("every inline variant is a member of the full registry", () => {
225
+ for (const variant of TEXT_INLINE_VARIANTS) {
226
+ expect(TEXT_VARIANTS).toContain(variant);
227
+ }
228
+ });
229
+ });
230
+
231
+ describe("resolveTextClass", () => {
232
+ test("falls back to the base treatment when there is nothing to go on", () => {
233
+ expect(resolveTextClass({})).toBe(TEXT_BASE_CLASS);
234
+ });
235
+
236
+ test("an inherited class beats the base", () => {
237
+ expect(resolveTextClass({ inherited: "text-2xl" })).toContain("text-2xl");
238
+ expect(resolveTextClass({ inherited: "text-2xl" })).not.toContain("text-base");
239
+ });
240
+
241
+ test("a named variant beats an inherited class", () => {
242
+ const paragraph = resolveTextClass({ variant: "paragraph" });
243
+ const nested = resolveTextClass({ inherited: paragraph, variant: "display" });
244
+ expect(fontSize(nested)).toBe("text-3xl");
245
+ });
246
+
247
+ test("a named colour beats an inherited colour", () => {
248
+ const parent = resolveTextClass({ color: "destructive" });
249
+ expect(colorOf(resolveTextClass({ color: "muted", inherited: parent }))).toBe("text-muted-foreground");
250
+ });
251
+
252
+ test("a className beats a named variant, a named colour and an inherited class", () => {
253
+ expect(fontSize(resolveTextClass({ className: "text-xs", variant: "display" }))).toBe("text-xs");
254
+ expect(colorOf(resolveTextClass({ className: "text-success", color: "destructive" }))).toBe("text-success");
255
+ expect(fontSize(resolveTextClass({ className: "text-xs", inherited: "text-3xl" }))).toBe("text-xs");
256
+ });
257
+
258
+ // The headline behaviour: React Native cascades a parent Text's style to a
259
+ // nested one natively, and this is what reproduces that through classNames.
260
+ test("an axis the child does not name is inherited", () => {
261
+ const parent = resolveTextClass({ color: "destructive", variant: "title" });
262
+ const child = resolveTextClass({ color: "muted", inherited: parent });
263
+
264
+ expect(fontSize(child)).toBe(fontSize(parent));
265
+ expect(weightOf(child)).toBe(weightOf(parent));
266
+ expect(colorOf(child)).toBe("text-muted-foreground");
267
+ });
268
+
269
+ // The Text.Strong case — it names a weight and nothing else.
270
+ test("a named weight leaves an inherited size and colour standing", () => {
271
+ const parent = resolveTextClass({ color: "muted", variant: "caption" });
272
+ const child = resolveTextClass({ inherited: parent, weight: "bold" });
273
+
274
+ expect(weightOf(child)).toBe("font-bold");
275
+ expect(fontSize(child)).toBe(fontSize(parent));
276
+ expect(colorOf(child)).toBe(colorOf(parent));
277
+ });
278
+
279
+ test("a named size beats the variant's own size", () => {
280
+ const cls = resolveTextClass({ size: "sm", variant: "header" });
281
+ expect(fontSize(cls)).toBe("text-sm");
282
+ expect(weightOf(cls)).toBe(weightOf(textVariants({ variant: "header" })));
283
+ });
284
+
285
+ test("resolves to exactly one font size, one weight and one colour", () => {
286
+ const cls = resolveTextClass({
287
+ className: "text-success",
288
+ color: "destructive",
289
+ inherited: "font-bold text-3xl text-muted-foreground",
290
+ size: "lg",
291
+ variant: "caption",
292
+ weight: "medium",
293
+ });
294
+
295
+ expect(cls.split(" ").filter((name) => TYPE_SCALE.includes(name))).toHaveLength(1);
296
+ expect(cls.split(" ").filter((name) => /^font-(normal|medium|semibold|bold)$/.test(name))).toHaveLength(1);
297
+ expect(cls.split(" ").filter((name) => name.startsWith("text-") && !TYPE_SCALE.includes(name))).toHaveLength(1);
298
+ });
299
+
300
+ test("transform none clears an overline's uppercase", () => {
301
+ const cls = resolveTextClass({ transform: "none", variant: "overline" });
302
+ expect(cls).toContain("normal-case");
303
+ expect(cls).not.toContain("uppercase");
304
+ });
305
+
306
+ // A standalone Text.Code is an ordinary Yoga box and takes padding. A nested
307
+ // one is a run inside the platform text engine, which ignores padding, margin
308
+ // and radius outright — so applying them would be a lie, not just wasted CSS.
309
+ test("code is padded only when it stands alone", () => {
310
+ expect(resolveTextClass({ variant: "code" })).toContain("px-1.5");
311
+ expect(resolveTextClass({ isNested: true, variant: "code" })).not.toContain("px-1.5");
312
+ expect(resolveTextClass({ isNested: true, variant: "code" })).toContain("font-mono");
313
+ });
314
+
315
+ test("an explicitly undefined axis is the same as omitting it", () => {
316
+ expect(resolveTextClass({ color: undefined, variant: undefined })).toBe(resolveTextClass({}));
317
+ });
318
+
319
+ test("an empty inherited string behaves as no inherited class", () => {
320
+ expect(resolveTextClass({ inherited: "" })).toBe(TEXT_BASE_CLASS);
321
+ });
322
+
323
+ test("carries through a className that is not typographic", () => {
324
+ const cls = resolveTextClass({ className: "mb-4" });
325
+ expect(cls).toContain("mb-4");
326
+ expect(fontSize(cls)).toBe("text-base");
327
+ });
328
+ });
329
+
330
+ // The correctness proof for the whole feature. If a nested Text with no props of
331
+ // its own resolves to anything but its parent's exact class string, inheritance
332
+ // is lossy somewhere — and the loss would be invisible until someone looked at a
333
+ // screen. Asserted across the matrix, and re-resolved so depth cannot drift.
334
+ describe("the nesting fixpoint", () => {
335
+ const CLASSNAMES = [undefined, "mb-4", "text-4xl", "italic underline"];
336
+
337
+ test("a nested Text with no props resolves to exactly its parent's class", () => {
338
+ for (const variant of TEXT_VARIANTS) {
339
+ for (const color of TEXT_COLORS) {
340
+ for (const className of CLASSNAMES) {
341
+ let parent = resolveTextClass({ className, color, variant });
342
+
343
+ for (let depth = 0; depth < 5; depth += 1) {
344
+ const child = resolveTextClass({ inherited: parent, isNested: true });
345
+ expect(child).toBe(parent);
346
+ parent = child;
347
+ }
348
+ }
349
+ }
350
+ }
351
+ });
352
+
353
+ test("the published class does not grow with depth", () => {
354
+ const root = resolveTextClass({ color: "destructive", variant: "display" });
355
+ let current = root;
356
+ for (let depth = 0; depth < 10; depth += 1) {
357
+ current = resolveTextClass({ inherited: current, isNested: true });
358
+ }
359
+ expect(current.split(" ")).toHaveLength(root.split(" ").length);
360
+ });
361
+ });
362
+
363
+ // Merge behaviour that is easy to mistake for a bug and "fix" into a real one.
364
+ describe("the inheritance contract with tailwind-merge", () => {
365
+ // Without this, a Button publishing nothing but `text-primary-foreground`
366
+ // would leave a nested Text with a colour and NO font size — silently
367
+ // collapsing to React Native's 14pt default. This is why the base class is
368
+ // applied unconditionally rather than guarded behind "am I nested".
369
+ test("the base still supplies the axes a partial provider left open", () => {
370
+ const cls = resolveTextClass({ inherited: "text-primary-foreground", isNested: true });
371
+ expect(fontSize(cls)).toBe("text-base");
372
+ expect(weightOf(cls)).toBe("font-normal");
373
+ expect(colorOf(cls)).toBe("text-primary-foreground");
374
+ });
375
+
376
+ // tailwind-merge conflicts a font size INTO line-height but not the reverse,
377
+ // so a size always wins and the leading vanishes. Documented, not fought.
378
+ test("a font size drops an inherited line height", () => {
379
+ expect(resolveTextClass({ inherited: "text-lg leading-7", size: "xs" })).not.toContain("leading-7");
380
+ });
381
+
382
+ // A bespoke `--text-*` token like `button-md` is registered in tokens.ts as a
383
+ // size. Unregistered it would land in the colour group and eat text-foreground.
384
+ test("a bespoke type token from another component merges as a size", () => {
385
+ const cls = resolveTextClass({ inherited: "text-button-md" });
386
+ expect(cls).toContain("text-button-md");
387
+ expect(cls).not.toContain("text-base");
388
+ expect(cls).toContain("text-foreground");
389
+ });
390
+
391
+ // `text-[17px]` and `text-(length:--x)` read as sizes; `text-[var(--x)]` is
392
+ // ambiguous and tailwind-merge files it under colour. Callers need to know.
393
+ test("an arbitrary size overrides the base only when written as a length", () => {
394
+ expect(resolveTextClass({ className: "text-[17px]" })).not.toContain("text-base");
395
+ expect(resolveTextClass({ className: "text-(length:--x)" })).not.toContain("text-base");
396
+ expect(resolveTextClass({ className: "text-[var(--x)]" })).toContain("text-base");
397
+ });
398
+ });
399
+
400
+ describe("canNestText", () => {
401
+ // A string or number child has no descendant component, so nothing below can
402
+ // read the context — publishing it would render a provider element per label
403
+ // for the most-instantiated component in the library.
404
+ test("a string or number child cannot hold a nested Text", () => {
405
+ expect(canNestText("Save")).toBe(false);
406
+ expect(canNestText(42)).toBe(false);
407
+ });
408
+
409
+ test("anything that can have a descendant can", () => {
410
+ expect(canNestText(["Save ", null])).toBe(true);
411
+ expect(canNestText(undefined)).toBe(true);
412
+ expect(canNestText(null)).toBe(true);
413
+ });
414
+ });
415
+
416
+ describe("TEXT_MAX_FONT_SIZE_MULTIPLIER", () => {
417
+ // A cap below 1 would shrink text at the OS default, which is the opposite of
418
+ // what a font-scaling cap is for.
419
+ test("caps growth without shrinking the default", () => {
420
+ expect(TEXT_MAX_FONT_SIZE_MULTIPLIER).toBeGreaterThan(1);
421
+ });
422
+ });