@cdx-ui/components 0.2.3 → 0.2.4

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 (248) hide show
  1. package/AGENTS.md +24 -0
  2. package/README.md +2 -0
  3. package/coverage.svg +1 -1
  4. package/docs/components/accordion/content.md +538 -0
  5. package/docs/components/alert/content.md +203 -0
  6. package/docs/components/alert-dialog/content.md +319 -0
  7. package/docs/components/avatar/content.md +368 -0
  8. package/docs/components/avatar/design.md +202 -0
  9. package/docs/components/badge/content.md +235 -0
  10. package/docs/components/bottom-sheet/content.md +351 -0
  11. package/docs/components/bottom-sheet/design.md +249 -0
  12. package/docs/components/breadcrumbs/content.md +169 -0
  13. package/docs/components/button/content.md +398 -0
  14. package/docs/components/button/design.md +258 -0
  15. package/docs/components/calendar/content.md +137 -0
  16. package/docs/components/card/content.md +283 -0
  17. package/docs/components/carousel/content.md +416 -0
  18. package/docs/components/checkbox/content.md +236 -0
  19. package/docs/components/checkbox/design.md +214 -0
  20. package/docs/components/chip/content.md +439 -0
  21. package/docs/components/chip/design.md +418 -0
  22. package/docs/components/color-area/content.md +86 -0
  23. package/docs/components/color-field/content.md +134 -0
  24. package/docs/components/color-picker/content.md +208 -0
  25. package/docs/components/color-slider/content.md +120 -0
  26. package/docs/components/color-swatch/content.md +64 -0
  27. package/docs/components/color-swatch-picker/content.md +126 -0
  28. package/docs/components/content.md +97 -0
  29. package/docs/components/currency-input/content.md +205 -0
  30. package/docs/components/data-grid/content.md +522 -0
  31. package/docs/components/date-field/content.md +147 -0
  32. package/docs/components/date-picker/content.md +360 -0
  33. package/docs/components/date-range-picker/content.md +301 -0
  34. package/docs/components/date-time-field/content.md +157 -0
  35. package/docs/components/date-time-list/content.md +105 -0
  36. package/docs/components/date-time-picker/content.md +272 -0
  37. package/docs/components/dialog/content.md +436 -0
  38. package/docs/components/dialog/design.md +192 -0
  39. package/docs/components/divider/content.md +112 -0
  40. package/docs/components/drawer/content.md +436 -0
  41. package/docs/components/field/content.md +277 -0
  42. package/docs/components/form/content.md +62 -0
  43. package/docs/components/full-screen-dialog/content.md +881 -0
  44. package/docs/components/heading/content.md +55 -0
  45. package/docs/components/icon/content.md +190 -0
  46. package/docs/components/icon-button/content.md +200 -0
  47. package/docs/components/icon-button/design.md +165 -0
  48. package/docs/components/image/content.md +104 -0
  49. package/docs/components/input/content.md +281 -0
  50. package/docs/components/input/design.md +389 -0
  51. package/docs/components/insurance-disclosure/content.md +103 -0
  52. package/docs/components/link/content.md +180 -0
  53. package/docs/components/link/design.md +160 -0
  54. package/docs/components/list-item/content.md +695 -0
  55. package/docs/components/menu/content.md +1247 -0
  56. package/docs/components/month-calendar/content.md +84 -0
  57. package/docs/components/month-picker/content.md +273 -0
  58. package/docs/components/otp-input/content.md +327 -0
  59. package/docs/components/progress-bar/content.md +87 -0
  60. package/docs/components/progress-circle/content.md +176 -0
  61. package/docs/components/progress-segmented/content.md +119 -0
  62. package/docs/components/quick-action-button/content.md +154 -0
  63. package/docs/components/quick-action-button/design.md +170 -0
  64. package/docs/components/radio/content.md +224 -0
  65. package/docs/components/range-calendar/content.md +123 -0
  66. package/docs/components/reorderable-list/content.md +309 -0
  67. package/docs/components/scoped-theme/content.md +47 -0
  68. package/docs/components/scroll-shadow/content.md +686 -0
  69. package/docs/components/select/content.md +400 -0
  70. package/docs/components/select/design.md +301 -0
  71. package/docs/components/skeleton/content.md +103 -0
  72. package/docs/components/slider/content.md +423 -0
  73. package/docs/components/stack/content.md +161 -0
  74. package/docs/components/switch/content.md +150 -0
  75. package/docs/components/switch/design.md +208 -0
  76. package/docs/components/table/content.md +548 -0
  77. package/docs/components/table-pagination/content.md +177 -0
  78. package/docs/components/tabs/content.md +292 -0
  79. package/docs/components/text/content.md +142 -0
  80. package/docs/components/textarea/content.md +222 -0
  81. package/docs/components/tile/content.md +703 -0
  82. package/docs/components/toast/content.md +456 -0
  83. package/docs/components/toggle-button/content.md +505 -0
  84. package/docs/components/tooltip/content.md +350 -0
  85. package/docs/components/virtualized-list/content.md +250 -0
  86. package/docs/foundations/content.md +9 -0
  87. package/docs/foundations/expressions/icons/content.md +19 -0
  88. package/docs/foundations/styles/tokens/all-tokens/content.md +192 -0
  89. package/docs/foundations/styles/tokens/what-are-tokens/content.md +33 -0
  90. package/docs/foundations/styles/tokens/what-are-tokens/design.md +33 -0
  91. package/docs/getting-started/content.md +4 -0
  92. package/docs/getting-started/designers/content.md +13 -0
  93. package/docs/getting-started/designers/design.md +13 -0
  94. package/docs/getting-started/developers/accessibility/content.md +31 -0
  95. package/docs/getting-started/developers/content.md +612 -0
  96. package/docs/getting-started/developers/styling/content.md +193 -0
  97. package/docs/llms.txt +193 -0
  98. package/docs/patterns/account-management/content.md +75 -0
  99. package/docs/patterns/authentication/content.md +52 -0
  100. package/docs/patterns/content.md +10 -0
  101. package/docs/patterns/dashboard/content.md +39 -0
  102. package/docs/patterns/empty-states/content.md +31 -0
  103. package/docs/patterns/forms/content.md +109 -0
  104. package/docs/patterns/onboarding/content.md +69 -0
  105. package/docs/patterns/payments/content.md +35 -0
  106. package/docs/patterns/settings/content.md +72 -0
  107. package/docs/resources/content.md +9 -0
  108. package/docs/resources/figma-libraries/content.md +42 -0
  109. package/docs/resources/release-notes/0.0.1-beta.118/content.md +27 -0
  110. package/docs/resources/release-notes/0.0.1-beta.122/content.md +21 -0
  111. package/docs/resources/release-notes/0.0.1-beta.124/content.md +16 -0
  112. package/docs/resources/release-notes/0.0.1-beta.126/content.md +10 -0
  113. package/docs/resources/release-notes/0.0.1-beta.127/content.md +14 -0
  114. package/docs/resources/release-notes/0.0.1-beta.128/content.md +9 -0
  115. package/docs/resources/release-notes/0.0.1-beta.129/content.md +18 -0
  116. package/docs/resources/release-notes/0.0.1-beta.130/content.md +18 -0
  117. package/docs/resources/release-notes/0.0.1-beta.133/content.md +7 -0
  118. package/docs/resources/release-notes/0.0.1-beta.134/content.md +17 -0
  119. package/docs/resources/release-notes/0.0.1-beta.136/content.md +7 -0
  120. package/docs/resources/release-notes/0.0.1-beta.137/content.md +10 -0
  121. package/docs/resources/release-notes/0.0.1-beta.138/content.md +7 -0
  122. package/docs/resources/release-notes/0.0.1-beta.139/content.md +13 -0
  123. package/docs/resources/release-notes/0.0.1-beta.140/content.md +26 -0
  124. package/docs/resources/release-notes/0.0.1-beta.141/content.md +63 -0
  125. package/docs/resources/release-notes/0.0.1-beta.142/content.md +7 -0
  126. package/docs/resources/release-notes/0.0.1-beta.143/content.md +12 -0
  127. package/docs/resources/release-notes/0.0.1-beta.144/content.md +7 -0
  128. package/docs/resources/release-notes/0.0.1-beta.145/content.md +7 -0
  129. package/docs/resources/release-notes/0.0.1-beta.147/content.md +10 -0
  130. package/docs/resources/release-notes/0.0.1-beta.149/content.md +10 -0
  131. package/docs/resources/release-notes/0.0.1-beta.150/content.md +13 -0
  132. package/docs/resources/release-notes/0.0.1-beta.152/content.md +7 -0
  133. package/docs/resources/release-notes/0.0.1-beta.154/content.md +15 -0
  134. package/docs/resources/release-notes/0.0.1-beta.156/content.md +7 -0
  135. package/docs/resources/release-notes/0.0.1-beta.158/content.md +9 -0
  136. package/docs/resources/release-notes/0.0.1-beta.159/content.md +11 -0
  137. package/docs/resources/release-notes/0.0.1-beta.160/content.md +7 -0
  138. package/docs/resources/release-notes/0.0.1-beta.161/content.md +7 -0
  139. package/docs/resources/release-notes/0.0.1-beta.163/content.md +7 -0
  140. package/docs/resources/release-notes/0.0.1-beta.164/content.md +9 -0
  141. package/docs/resources/release-notes/0.0.1-beta.166/content.md +17 -0
  142. package/docs/resources/release-notes/0.0.1-beta.167/content.md +7 -0
  143. package/docs/resources/release-notes/0.0.1-beta.168/content.md +7 -0
  144. package/docs/resources/release-notes/0.0.1-beta.169/content.md +7 -0
  145. package/docs/resources/release-notes/0.0.1-beta.171/content.md +16 -0
  146. package/docs/resources/release-notes/0.0.1-beta.172/content.md +24 -0
  147. package/docs/resources/release-notes/0.0.1-beta.173/content.md +15 -0
  148. package/docs/resources/release-notes/0.0.1-beta.174/content.md +7 -0
  149. package/docs/resources/release-notes/0.0.1-beta.176/content.md +32 -0
  150. package/docs/resources/release-notes/0.0.1-beta.179/content.md +11 -0
  151. package/docs/resources/release-notes/0.0.1-beta.182/content.md +13 -0
  152. package/docs/resources/release-notes/0.0.1-beta.185/content.md +7 -0
  153. package/docs/resources/release-notes/0.0.1-beta.187/content.md +27 -0
  154. package/docs/resources/release-notes/0.1.1/content.md +23 -0
  155. package/docs/resources/release-notes/0.1.2/content.md +7 -0
  156. package/docs/resources/release-notes/0.1.4/content.md +19 -0
  157. package/docs/resources/release-notes/0.1.5/content.md +7 -0
  158. package/docs/resources/release-notes/0.1.6/content.md +7 -0
  159. package/docs/resources/release-notes/0.2.0/content.md +31 -0
  160. package/docs/resources/release-notes/0.2.1/content.md +17 -0
  161. package/docs/resources/release-notes/0.2.2/content.md +11 -0
  162. package/docs/resources/release-notes/0.2.3/content.md +15 -0
  163. package/docs/resources/release-notes/content.md +64 -0
  164. package/lib/commonjs/components/DateTimeList/index.js +11 -26
  165. package/lib/commonjs/components/DateTimeList/index.js.map +1 -1
  166. package/lib/commonjs/components/DateTimeList/styles.js +2 -12
  167. package/lib/commonjs/components/DateTimeList/styles.js.map +1 -1
  168. package/lib/commonjs/components/ListItem/index.js.map +1 -1
  169. package/lib/commonjs/components/ListItem/styles.js +2 -1
  170. package/lib/commonjs/components/ListItem/styles.js.map +1 -1
  171. package/lib/commonjs/components/Menu/index.js +11 -5
  172. package/lib/commonjs/components/Menu/index.js.map +1 -1
  173. package/lib/commonjs/components/ScrollShadow/gradient.js +29 -0
  174. package/lib/commonjs/components/ScrollShadow/gradient.js.map +1 -0
  175. package/lib/commonjs/components/ScrollShadow/index.js +171 -44
  176. package/lib/commonjs/components/ScrollShadow/index.js.map +1 -1
  177. package/lib/commonjs/components/ScrollShadow/styles.js +58 -14
  178. package/lib/commonjs/components/ScrollShadow/styles.js.map +1 -1
  179. package/lib/commonjs/components/ScrollShadow/useComposedScrollHandler.js +63 -0
  180. package/lib/commonjs/components/ScrollShadow/useComposedScrollHandler.js.map +1 -0
  181. package/lib/commonjs/components/ScrollShadow/useScrollShadow.js +48 -8
  182. package/lib/commonjs/components/ScrollShadow/useScrollShadow.js.map +1 -1
  183. package/lib/commonjs/components/Tabs/index.js +16 -23
  184. package/lib/commonjs/components/Tabs/index.js.map +1 -1
  185. package/lib/commonjs/components/Tabs/styles.js +1 -10
  186. package/lib/commonjs/components/Tabs/styles.js.map +1 -1
  187. package/lib/commonjs/figma/ListItem.SectionHeader.figma.js +20 -5
  188. package/lib/commonjs/figma/ListItem.SectionHeader.figma.js.map +1 -1
  189. package/lib/module/components/DateTimeList/index.js +12 -27
  190. package/lib/module/components/DateTimeList/index.js.map +1 -1
  191. package/lib/module/components/DateTimeList/styles.js +1 -11
  192. package/lib/module/components/DateTimeList/styles.js.map +1 -1
  193. package/lib/module/components/ListItem/index.js.map +1 -1
  194. package/lib/module/components/ListItem/styles.js +2 -1
  195. package/lib/module/components/ListItem/styles.js.map +1 -1
  196. package/lib/module/components/Menu/index.js +11 -5
  197. package/lib/module/components/Menu/index.js.map +1 -1
  198. package/lib/module/components/ScrollShadow/gradient.js +24 -0
  199. package/lib/module/components/ScrollShadow/gradient.js.map +1 -0
  200. package/lib/module/components/ScrollShadow/index.js +170 -48
  201. package/lib/module/components/ScrollShadow/index.js.map +1 -1
  202. package/lib/module/components/ScrollShadow/styles.js +57 -13
  203. package/lib/module/components/ScrollShadow/styles.js.map +1 -1
  204. package/lib/module/components/ScrollShadow/useComposedScrollHandler.js +57 -0
  205. package/lib/module/components/ScrollShadow/useComposedScrollHandler.js.map +1 -0
  206. package/lib/module/components/ScrollShadow/useScrollShadow.js +46 -8
  207. package/lib/module/components/ScrollShadow/useScrollShadow.js.map +1 -1
  208. package/lib/module/components/Tabs/index.js +17 -24
  209. package/lib/module/components/Tabs/index.js.map +1 -1
  210. package/lib/module/components/Tabs/styles.js +0 -9
  211. package/lib/module/components/Tabs/styles.js.map +1 -1
  212. package/lib/module/figma/ListItem.SectionHeader.figma.js +20 -5
  213. package/lib/module/figma/ListItem.SectionHeader.figma.js.map +1 -1
  214. package/lib/typescript/components/DateTimeList/index.d.ts.map +1 -1
  215. package/lib/typescript/components/DateTimeList/styles.d.ts +1 -4
  216. package/lib/typescript/components/DateTimeList/styles.d.ts.map +1 -1
  217. package/lib/typescript/components/ListItem/index.d.ts +5 -4
  218. package/lib/typescript/components/ListItem/index.d.ts.map +1 -1
  219. package/lib/typescript/components/ListItem/styles.d.ts +1 -1
  220. package/lib/typescript/components/ListItem/styles.d.ts.map +1 -1
  221. package/lib/typescript/components/Menu/index.d.ts.map +1 -1
  222. package/lib/typescript/components/ScrollShadow/gradient.d.ts +7 -0
  223. package/lib/typescript/components/ScrollShadow/gradient.d.ts.map +1 -0
  224. package/lib/typescript/components/ScrollShadow/index.d.ts +57 -15
  225. package/lib/typescript/components/ScrollShadow/index.d.ts.map +1 -1
  226. package/lib/typescript/components/ScrollShadow/styles.d.ts +8 -2
  227. package/lib/typescript/components/ScrollShadow/styles.d.ts.map +1 -1
  228. package/lib/typescript/components/ScrollShadow/useComposedScrollHandler.d.ts +13 -0
  229. package/lib/typescript/components/ScrollShadow/useComposedScrollHandler.d.ts.map +1 -0
  230. package/lib/typescript/components/ScrollShadow/useScrollShadow.d.ts +15 -2
  231. package/lib/typescript/components/ScrollShadow/useScrollShadow.d.ts.map +1 -1
  232. package/lib/typescript/components/Tabs/index.d.ts.map +1 -1
  233. package/lib/typescript/components/Tabs/styles.d.ts +0 -7
  234. package/lib/typescript/components/Tabs/styles.d.ts.map +1 -1
  235. package/lib/typescript/figma/ListItem.SectionHeader.figma.d.ts.map +1 -1
  236. package/package.json +8 -6
  237. package/src/components/DateTimeList/index.tsx +13 -26
  238. package/src/components/DateTimeList/styles.ts +1 -10
  239. package/src/components/ListItem/index.tsx +5 -4
  240. package/src/components/ListItem/styles.ts +1 -0
  241. package/src/components/Menu/index.tsx +11 -8
  242. package/src/components/ScrollShadow/gradient.ts +48 -0
  243. package/src/components/ScrollShadow/index.tsx +300 -69
  244. package/src/components/ScrollShadow/styles.ts +45 -28
  245. package/src/components/ScrollShadow/useComposedScrollHandler.ts +58 -0
  246. package/src/components/ScrollShadow/useScrollShadow.ts +72 -10
  247. package/src/components/Tabs/index.tsx +25 -35
  248. package/src/components/Tabs/styles.ts +0 -9
@@ -0,0 +1,612 @@
1
+ # Installation (https://design.candescent.com/docs/getting-started/developers)
2
+
3
+ ## Expo
4
+
5
+ ### Install packages
6
+
7
+ Install the core packages in your Expo / React Native project:
8
+
9
+ ```bash
10
+ pnpm add @cdx-ui/components @cdx-ui/icons @cdx-ui/styles @cdx-ui/native uniwind tailwindcss
11
+ # or: npm install @cdx-ui/components @cdx-ui/icons @cdx-ui/styles @cdx-ui/native uniwind tailwindcss
12
+ ```
13
+
14
+ - **`@cdx-ui/styles`** — provides the design token CSS files (`theme.css`, `utilities.css`) imported in `global.css`.
15
+ - **`@cdx-ui/native`** — native platform integration: the Metro config wrapper (`@cdx-ui/native/metro`) and the Expo config plugin that embeds Forge fonts (listed as `"@cdx-ui/native"` in `app.json` plugins). It ships **no root export** — always import from a subpath.
16
+ - **`tailwindcss`** v4 — required by the CSS entry file (`@import 'tailwindcss'`).
17
+ - **`uniwind`** — required by the CSS entry file (`@import 'uniwind'`) and by the Metro transformer for React Native styling.
18
+
19
+ #### Packages
20
+
21
+ [@cdx-ui/components](https://www.npmjs.com/package/@cdx-ui/components) — Styled UI components (Button, Input, Dialog, etc.)
22
+
23
+ [@cdx-ui/icons](https://www.npmjs.com/package/@cdx-ui/icons) — Icon library with Material Symbols SVGs
24
+
25
+ [@cdx-ui/primitives](https://www.npmjs.com/package/@cdx-ui/primitives) — Unstyled behavioral components (a11y, interactions)
26
+
27
+ [@cdx-ui/styles](https://www.npmjs.com/package/@cdx-ui/styles) — Design tokens and themes
28
+
29
+ [@cdx-ui/native](https://www.npmjs.com/package/@cdx-ui/native) — Metro config wrapper and Expo font loading
30
+
31
+ [@cdx-ui/utils](https://www.npmjs.com/package/@cdx-ui/utils) — Shared helpers (styling utilities, event handlers, refs)
32
+
33
+ #### Peer dependencies
34
+
35
+ Make sure the following are already installed (Expo projects include most of them by default):
36
+
37
+ | Package | Version | Required by |
38
+ | -------------------------------- | ------------------- | ------------------------------------- |
39
+ | `react` | ^18.2.0 or ^19.0.0 | all packages |
40
+ | `react-native` | >=0.76.0 | all packages |
41
+ | `react-native-svg` | ^13 / ^14 / ^15 | `@cdx-ui/icons` |
42
+ | `react-native-safe-area-context` | >=4.0.0 | `@cdx-ui/components` |
43
+ | `react-native-reanimated` | >=4.0.0 (optional) | `@cdx-ui/components` |
44
+ | `react-native-worklets` | >=0.4.0 (optional) | `@cdx-ui/components` |
45
+ | `react-native-gesture-handler` | >=2.0.0 (optional) | `@cdx-ui/components` |
46
+ | `react-native-web` | >=0.19.0 (optional) | `@cdx-ui/components` |
47
+ | `@shopify/flash-list` | >=2.0.0 (optional) | `@cdx-ui/components` |
48
+ | `react-native-sortables` | ^1.10.1 (optional) | `@cdx-ui/components/reorderable-list` |
49
+ | `@dnd-kit/core` | ^6.3.0 (optional) | `@cdx-ui/components/reorderable-list` |
50
+ | `@dnd-kit/sortable` | ^10.0.0 (optional) | `@cdx-ui/components/reorderable-list` |
51
+ | `@dnd-kit/utilities` | ^3.2.0 (optional) | `@cdx-ui/components/reorderable-list` |
52
+ | `expo` | >=52.0.0 (optional) | `@cdx-ui/native` |
53
+ | `metro-config` | >=0.80.0 (optional) | `@cdx-ui/native` |
54
+ | `tailwindcss` | v4 | build tooling |
55
+
56
+ - **`react-native-safe-area-context`** — required by `SafeAreaView` and `useSafeAreaInsets` re-exports; most Expo projects include it. You still need a root `SafeAreaProvider` even when using `ForgeDesignProvider`.
57
+ - **`react-native-reanimated`** / **`react-native-worklets`** — required by animated components (`Dialog`, `AlertDialog`, `Toast`, `BottomSheet`, `Skeleton`, `Carousel`, `Tabs`, and Select overlays). Worklets is Reanimated's companion runtime.
58
+ - **`react-native-gesture-handler`** — required for Dialog/AlertDialog swipe-to-dismiss, Toast swipe gestures, and BottomSheet on native. Wrap the app root in a `GestureHandlerRootView` (see [step 4](#4-import-css-and-wrap-the-app-root)); without it, opening a `Dialog`, `AlertDialog`, `BottomSheet`, or `Toast` on native throws in development.
59
+ - **`react-native-web`** — needed when targeting web; Expo web projects include it automatically.
60
+ - **`@shopify/flash-list`** — only required if using `VirtualizedList`.
61
+ - **`react-native-sortables`** (native) and **`@dnd-kit/core`**, **`@dnd-kit/sortable`**, **`@dnd-kit/utilities`** (web) — only required if you import `ReorderableList` from `@cdx-ui/components/reorderable-list`; the main `@cdx-ui/components` entry never loads them. See [ReorderableList installation](https://design.candescent.com/docs/components/reorderable-list/content.md#installation).
62
+ - **`expo`** — only required by the `@cdx-ui/native` font config plugin; every Expo project already has it. Browser-only apps do not need it: they load fonts through `@cdx-ui/styles/fonts`.
63
+ - **`metro-config`** — only required by `@cdx-ui/native/metro`; every Expo/React Native project already has it.
64
+
65
+ > [!WARNING]
66
+ >
67
+ > **Uniwind requirement**
68
+ >
69
+ > While the published `peerDependencies` floor is `react-native >=0.76.0`, Forge UI's Tailwind-based styling (via Uniwind) requires **React Native 0.81+** (New Architecture) and **Expo SDK 54+**. Projects on earlier versions will satisfy the peer dep check but cannot use the styling pipeline.
70
+
71
+ ---
72
+
73
+ ### Metro Setup (React Native / Expo)
74
+
75
+ Forge UI uses Tailwind-based styling via [Uniwind](https://github.com/uni-stack/uniwind). A helper wraps your Metro config so you don't need to configure Uniwind directly.
76
+
77
+ #### 1. Create a CSS entry file
78
+
79
+ Create `global.css` in your project root:
80
+
81
+ ```css
82
+ @import 'tailwindcss';
83
+ @import 'uniwind';
84
+
85
+ @custom-variant dark (&:where(.dark, .dark *));
86
+
87
+ @import '@cdx-ui/styles/theme.css';
88
+ @import '@cdx-ui/styles/utilities.css';
89
+ @import '@cdx-ui/components/styles.css';
90
+ ```
91
+
92
+ - **`@import 'uniwind'`** — Registers Uniwind's CSS transforms and plugins with Tailwind. Must appear immediately after `@import 'tailwindcss'` — before any other directives or theme imports.
93
+ - **`@custom-variant dark`** — Switches Tailwind v4's dark mode from `@media (prefers-color-scheme)` to class-based activation. Required for `ScopedTheme` to work on web. Harmless if you don't use `ScopedTheme` — global dark mode continues to work via `Uniwind.setTheme()`.
94
+ - **`@cdx-ui/styles/theme.css`** — Design tokens (colors, spacing, radii, shadows, fonts) as a Tailwind v4 `@theme static` block, plus light/dark mode and platform font overrides via `@variant`.
95
+ - **`@cdx-ui/styles/utilities.css`** — Composite typography utilities (`heading-xl`…`heading-xs`, `body-xl`…`body-xs`, `label-sm`, `label-xs`) used by `Heading`, `Text`, and `Link`. Each has a `-size` companion (`body-sm-size`) that sets only the font size, line height, and letter spacing, so nested text can change size and keep its inherited font family and weight.
96
+ - **`@cdx-ui/components/styles.css`** — `@source` directive telling Tailwind where to scan for component utility classes.
97
+
98
+ > [!NOTE]
99
+ >
100
+ > **Non-Tailwind consumers**
101
+ >
102
+ > Import `@cdx-ui/styles/vanilla.css` for a plain CSS file with `:root` variables and utility
103
+ > classes — no Tailwind `@theme`/`@variant` syntax required.
104
+
105
+ > [!WARNING]
106
+ >
107
+ > **Web consumers: @cdx-ui/styles/web.css is required**
108
+ >
109
+ > Every browser app must import `@cdx-ui/styles/web.css` as a **separate JS import** — not via `@import` in your CSS file — after your Tailwind CSS entry. It bridges a cascade-layer gap in Tailwind v4's `@theme static` output (unlayered `:root` beats anything inside `@layer theme`), and carries the unlayered `.dark`/`.light` overrides the layered output cannot, for apps that toggle dark mode via a `.dark` class (for example with `next-themes` or a manual toggle).
110
+ >
111
+ > ```tsx
112
+ > import './global.css'; // Tailwind + Uniwind CSS entry
113
+ > import '@cdx-ui/styles/web.css'; // Unlayered token overrides — MUST be after Tailwind
114
+ > ```
115
+
116
+ #### 2. Configure Metro
117
+
118
+ In `metro.config.js`, wrap your config with `withForgeMetroConfig`:
119
+
120
+ ```js
121
+ const { getDefaultConfig } = require('expo/metro-config');
122
+ const { withForgeMetroConfig } = require('@cdx-ui/native/metro');
123
+
124
+ const config = getDefaultConfig(__dirname);
125
+
126
+ module.exports = withForgeMetroConfig(config, {
127
+ cssEntryFile: './global.css',
128
+ generatedTypesFile: './uniwind-types.d.ts', // optional, enables className autocomplete
129
+ });
130
+ ```
131
+
132
+ > [!NOTE]
133
+ >
134
+ > `withForgeMetroConfig` must be the **outermost** wrapper if you compose multiple Metro config
135
+ > helpers.
136
+
137
+ #### 3. Add the font config plugin
138
+
139
+ Add `@cdx-ui/native` to the `plugins` array in `app.json`, then rebuild the native app (`npx expo prebuild`, `expo run:ios` / `expo run:android`, or an EAS build):
140
+
141
+ ```json
142
+ {
143
+ "expo": {
144
+ "plugins": ["@cdx-ui/native"]
145
+ }
146
+ }
147
+ ```
148
+
149
+ The plugin embeds every Forge font family into the iOS and Android builds, each registered as one family carrying all of its faces. That is what lets `font-medium`, `font-semibold`, `font-bold`, and `italic` select a real face on native, the same way `font-weight` and `font-style` do in a browser. Fonts are part of the binary, so there is no loading state to wait for. Config plugins need a [development build](https://docs.expo.dev/develop/development-builds/introduction/); Expo Go cannot load them.
150
+
151
+ Font selection is handled entirely by the CSS variable layer, including a `ThemeOverride` that switches the Display family at runtime.
152
+
153
+ > [!NOTE]
154
+ >
155
+ > **Upgrading from useForgeFonts**
156
+ >
157
+ > `useForgeFonts` loaded fonts at runtime through `expo-font`, which registers each face as a separate family. On Android that shape cannot carry weight or italic. It is deprecated and now always reports `loaded`: add the plugin, rebuild, and remove the hook along with any rendering gate on it. An Expo app that also exports to web keeps calling `loadForgeWebFonts` from [`@cdx-ui/styles/fonts`](#fonts-on-web), which is inert off the browser.
158
+
159
+ #### 4. Import CSS and wrap the app root
160
+
161
+ In your root layout (e.g. `app/_layout.tsx`), import the CSS entry file and wrap the tree with `GestureHandlerRootView`, `SafeAreaProvider`, and `ForgeDesignProvider`.
162
+
163
+ > [!NOTE]
164
+ >
165
+ > The example below uses [Expo Router](https://docs.expo.dev/router/introduction/) (`expo-router`)
166
+ > for navigation. If your project uses a different router, replace the `Stack` import with your own
167
+ > root navigator.
168
+
169
+ ```tsx
170
+ import '../global.css';
171
+
172
+ import { Stack } from 'expo-router';
173
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
174
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
175
+ import { ForgeDesignProvider } from '@cdx-ui/components';
176
+
177
+ export default function RootLayout() {
178
+ return (
179
+ <GestureHandlerRootView style={{ flex: 1 }}>
180
+ <SafeAreaProvider>
181
+ <ForgeDesignProvider>
182
+ <Stack />
183
+ </ForgeDesignProvider>
184
+ </SafeAreaProvider>
185
+ </GestureHandlerRootView>
186
+ );
187
+ }
188
+ ```
189
+
190
+ `GestureHandlerRootView` must sit above `ForgeDesignProvider`. Overlays render through the provider's portal host, so the `Dialog`, `AlertDialog`, `BottomSheet`, and `Toast` gestures need the root view as an ancestor of that host, not just of the screen that opens them.
191
+
192
+ `ForgeDesignProvider` mounts shared app-root services:
193
+
194
+ - `PortalHost` — required for `Dialog` / `AlertDialog` (without a host, overlays fail silently)
195
+ - `ToastProvider` — required for `useToast()` (disable with `config={{ toast: false }}` if unused)
196
+ - Global animation settings and text scaling props (`config.animation`, `config.textProps`)
197
+ - `SafeAreaListener` → keeps Uniwind insets in sync
198
+
199
+ It does **not** replace `SafeAreaProvider`. If you skip `ForgeDesignProvider`, mount one `<PortalHost />` from `@cdx-ui/primitives` (and `ToastProvider` when using toasts) yourself — and do not add a second host when the provider is already in the tree.
200
+
201
+ ---
202
+
203
+ ### Minimal Example
204
+
205
+ ```tsx
206
+ import { Button, Heading, Text, VStack } from '@cdx-ui/components';
207
+
208
+ export default function HomeScreen() {
209
+ return (
210
+ <VStack space="lg" className="flex-1 p-6">
211
+ <Heading>Welcome</Heading>
212
+ <Text>Get started with Forge UI.</Text>
213
+ <Button onPress={() => {}}>
214
+ <Button.Label>Continue</Button.Label>
215
+ </Button>
216
+ </VStack>
217
+ );
218
+ }
219
+ ```
220
+
221
+ Components carry their own look, so the example sets no colors or typography. Before you style a screen, read [Styling and customization](https://design.candescent.com/docs/getting-started/developers/styling/content.md).
222
+
223
+ ---
224
+
225
+ ### React Native re-exports
226
+
227
+ `@cdx-ui/components` re-exports several core React Native components and hooks so consuming apps can import everything from a single package:
228
+
229
+ ```tsx
230
+ import {
231
+ View,
232
+ ScrollView,
233
+ KeyboardAvoidingView,
234
+ SafeAreaView,
235
+ useSafeAreaInsets,
236
+ } from '@cdx-ui/components';
237
+ ```
238
+
239
+ | Export | Source |
240
+ | ---------------------- | -------------------------------- |
241
+ | `View` | `react-native` |
242
+ | `ScrollView` | `react-native` |
243
+ | `KeyboardAvoidingView` | `react-native` |
244
+ | `SafeAreaView` | `react-native-safe-area-context` |
245
+ | `useSafeAreaInsets` | `react-native-safe-area-context` |
246
+
247
+ These are convenience re-exports of the underlying packages — not Uniwind wrappers. `className` works on RN views when Uniwind's Metro pipeline is configured; importing from `@cdx-ui/components` vs `react-native` does not change that.
248
+
249
+ ---
250
+
251
+ ### Next steps
252
+
253
+ [Components reference](https://design.candescent.com/docs/components/content.md) — detailed docs for every component\
254
+ [Styling and customization](https://design.candescent.com/docs/getting-started/developers/styling/content.md) — what to style, and what to leave to the components\
255
+ [Accessibility](https://design.candescent.com/docs/getting-started/developers/accessibility/content.md) — what the components handle, and what your screen still provides\
256
+ [Icons](https://design.candescent.com/docs/foundations/expressions/icons/content.md) — using `@cdx-ui/icons`\
257
+ [Link / navigation](https://design.candescent.com/docs/components/link/content.md) — router integration with `LinkProvider`
258
+
259
+ ## Vite
260
+
261
+ Use this path when you want Forge UI components in a standard Vite + React + TypeScript web app. Forge UI is authored against React Native primitives; on web those resolve through `react-native-web` in the same browser bundle as your existing React components.
262
+
263
+ ### Create a project
264
+
265
+ Scaffold a new app with the React + TypeScript template:
266
+
267
+ ```bash
268
+ npm create vite@latest my-forge-app -- --template react-ts
269
+ cd my-forge-app
270
+ ```
271
+
272
+ Use `pnpm create vite@latest` or `yarn create vite` if you prefer those package managers.
273
+
274
+ ### Install dependencies
275
+
276
+ Install all five runtime `@cdx-ui/*` packages at the **same version** — even if you only import from `@cdx-ui/components` directly. Mismatched versions can fork the renderer and cause confusing runtime errors. `@cdx-ui/native` is not needed here: it is the Metro/Expo integration package.
277
+
278
+ ```bash
279
+ pnpm add @cdx-ui/components @cdx-ui/icons @cdx-ui/primitives @cdx-ui/styles @cdx-ui/utils react-native-web react-native-reanimated react-native-worklets react-native-gesture-handler react-native-safe-area-context react-native-svg
280
+ # or: npm install @cdx-ui/components @cdx-ui/icons @cdx-ui/primitives @cdx-ui/styles @cdx-ui/utils react-native-web react-native-reanimated react-native-worklets react-native-gesture-handler react-native-safe-area-context react-native-svg
281
+ ```
282
+
283
+ Add the Forge Vite integration and its build tooling:
284
+
285
+ ```bash
286
+ pnpm add @cdx-ui/web @tailwindcss/vite tailwindcss uniwind react-native -D
287
+ # or: npm install @cdx-ui/web @tailwindcss/vite tailwindcss uniwind react-native -D
288
+ ```
289
+
290
+ `@cdx-ui/components` owns the React Native runtime peer ranges in the first command. Install `@cdx-ui/web` from the same Forge UI release; it owns the Vite integration and declares `vite`, `@tailwindcss/vite`, `tailwindcss`, and `uniwind` as its build-time peers. The Vite scaffold already includes `vite` and `@vitejs/plugin-react`.
291
+
292
+ | Package | Role |
293
+ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
294
+ | `@cdx-ui/web` | Build-time Forge UI integration for Vite |
295
+ | `react-native-web` | Web rendering target for React Native primitives |
296
+ | `react-native-reanimated` | Required by animated Forge UI components (`Dialog`, `Toast`, `Skeleton`, `Carousel`, `Tabs`, and related overlays) |
297
+ | `react-native-worklets` | Companion runtime for Reanimated 4 |
298
+ | `react-native-gesture-handler` | Gesture support for Dialog swipe-to-dismiss and Toast swipe |
299
+ | `react-native-safe-area-context` | Required peer of `@cdx-ui/components` |
300
+ | `react-native-svg` | Required transitively by `@cdx-ui/icons`; resolved to its web entry by `@cdx-ui/web/vite` |
301
+ | `uniwind` | Compiles `className` strings into CSS on web |
302
+ | `react-native` (dev) | TypeScript types only — the Forge Vite integration resolves `react-native` imports through Uniwind's web implementation |
303
+
304
+ #### Fonts on web
305
+
306
+ Browser apps load their own fonts — `@cdx-ui/native` is the Metro/Expo integration and should not be installed here. What Forge provides is the answer to _which_ fonts, generated from the token pipeline so it cannot drift from `theme.css`:
307
+
308
+ ```tsx
309
+ import { forgeWebFontSpec } from '@cdx-ui/styles/fonts';
310
+
311
+ forgeWebFontSpec.platform; // --font-sans, --font-mono — e.g. Inter, IBM Plex Mono
312
+ forgeWebFontSpec.display; // every Display family a ThemeOverride can select
313
+ ```
314
+
315
+ Each entry is a **base family** with the weights and italics it actually ships (`{ family, normalWeights, italicWeights }`). Register the family once per weight and let `font-weight` select the face — that is how a browser resolves weight, and it is what makes `font-medium`, `font-semibold` and `italic` reach the right file.
316
+
317
+ Register them statically, in your document head, so the browser's preload scanner finds them while parsing HTML rather than after your bundle evaluates. `buildWebFontProviderUrl` gives you the href:
318
+
319
+ ```tsx
320
+ import { buildWebFontProviderUrl, forgeWebFontSpec } from '@cdx-ui/styles/fonts';
321
+
322
+ // Log it once, paste the result into index.html. Most apps ship one Display
323
+ // family, so there is no need to request all of them:
324
+ buildWebFontProviderUrl([...forgeWebFontSpec.platform, forgeWebFontSpec.display[0]]);
325
+ ```
326
+
327
+ > [!WARNING]
328
+ >
329
+ > **Ask only for weights a family actually has**
330
+ >
331
+ > A font provider rejects the **entire** request if any family is asked for a weight it does not ship — one bad axis silently drops every font in that URL. Availability differs per family (Libre Caslon Text has 400 and 700 only, with italic at 400 only), which is why the weights travel with the family in `forgeWebFontSpec` rather than being a fixed 400/500/600/700 list.
332
+
333
+ **Self-hosting.** If your deployment cannot call an external host, write the `@font-face` rules yourself against files you host. The shape that matters is one rule per face, declared under the **base family**, with `font-weight` and `font-style` descriptors:
334
+
335
+ ```css
336
+ @font-face {
337
+ font-family: 'Inter';
338
+ src: url('/fonts/Inter-500.woff2') format('woff2');
339
+ font-weight: 500;
340
+ font-style: normal;
341
+ font-display: swap;
342
+ }
343
+ ```
344
+
345
+ Declaring a face under its own family name instead (`font-family: 'Inter Medium'`) will not work — `font-weight` has nothing to select against.
346
+
347
+ **Fonts chosen at runtime.** If the family is not known until runtime — an FI font picker, or a live theme preview — a static tag cannot cover it. `loadForgeWebFonts` injects the `<link>` and returns a cleanup function:
348
+
349
+ ```tsx
350
+ import { loadForgeWebFonts, forgeWebFontSpec } from '@cdx-ui/styles/fonts';
351
+
352
+ useEffect(() => loadForgeWebFonts({ families: [selectedDisplayFamily] }), [selectedDisplayFamily]);
353
+ ```
354
+
355
+ > [!NOTE]
356
+ >
357
+ > **Weight and style, not face families**
358
+ >
359
+ > Font tokens name one family per role — `--font-sans`, `--font-body`, `--font-mono`, `--font-display`. On `Text`, set weight and style with the `weight` and `italic` props, which select the right face on every platform:
360
+ >
361
+ > ```tsx
362
+ > <Text weight="semibold" italic />
363
+ > ```
364
+ >
365
+ > Outside `Text`, the `font-medium` / `font-semibold` / `font-bold` / `italic` classes do the same over the family a composite typography utility already applies.
366
+ >
367
+ > Some Display families ship no italic faces, and an FI can switch the Display family at runtime. Style italic Display text with `[font-style:var(--font-display-italic-style)]`, which is `normal` for those families, so it renders upright at the right weight instead of each platform improvising a slant.
368
+
369
+ ### Dependency footprint
370
+
371
+ Only `react` and `react-native-safe-area-context` are hard-required peers of `@cdx-ui/components`; the remaining React Native runtime peers are optional and enter the bundle according to the components you use.
372
+
373
+ | Package | min (KB) | gzip (KB) |
374
+ | -------------------------------- | ---------: | --------: |
375
+ | `react-native-web` | 521.4 | 160.9 |
376
+ | `react-native-reanimated` | 472.7 | 148.0 |
377
+ | `react-native-gesture-handler` | 290.2 | 94.3 |
378
+ | `uniwind` (runtime) | 67.6 | 30.6 |
379
+ | `react-native-worklets` | 51.1 | 18.6 |
380
+ | `react-native-svg` | 77.4 | 16.6 |
381
+ | `react-native-safe-area-context` | 0.4 | 0.3 |
382
+ | **RN + styling subtotal** | **1480.9** | **469.4** |
383
+
384
+ These measured contributions from a component-heavy production Vite build are an upper bound: React Native Web is the web baseline, while Reanimated is the conditional heavy entry and is currently unoptimized on Vite. At 469.4 KB gzip for the RN + styling subtotal, the footprint is comparable to a mainstream web component kit; these are shipped bytes, not `node_modules` disk sizes.
385
+
386
+ ### Configure Vite
387
+
388
+ Replace `vite.config.ts` with the following:
389
+
390
+ ```ts
391
+ import { defineConfig } from 'vite';
392
+ import react from '@vitejs/plugin-react';
393
+ import { forge } from '@cdx-ui/web/vite';
394
+
395
+ export default defineConfig({
396
+ plugins: [forge({ cssEntryFile: 'src/index.css' }), react()],
397
+ });
398
+ ```
399
+
400
+ `forge()` owns the `__DEV__` and `process.env.NODE_ENV` defines, `react-native` and `react-native-svg` aliases, `.web.*` resolution, Tailwind `optimize: false` plus Uniwind ordering, dependency pre-bundling, package deduplication, Rollup compatibility settings, and the React Native Worklets compatibility patch. Keep `react()` after `forge()` so the owned sub-plugins run in the required order.
401
+
402
+ - **`cssEntryFile`** — Path to the app-owned CSS entry described below; `forge()` passes it to Uniwind but does not create or import the file.
403
+ - **`patchWorklets`** — Controls the React Native Worklets compatibility patch. It defaults to `true`; pass `false` only when you need to disable that patch for your installed Worklets version.
404
+
405
+ ### CSS entry
406
+
407
+ Replace `src/index.css` with:
408
+
409
+ ```css
410
+ @import 'tailwindcss/theme';
411
+ @import 'tailwindcss/utilities';
412
+ @import 'uniwind';
413
+
414
+ @custom-variant dark (&:where(.dark, .dark *));
415
+
416
+ @import '@cdx-ui/styles/theme.css';
417
+ @import '@cdx-ui/styles/utilities.css';
418
+ @import '@cdx-ui/components/styles.css';
419
+
420
+ body {
421
+ background-color: var(--color-surface-primary);
422
+ color: var(--color-content-primary);
423
+ }
424
+ ```
425
+
426
+ Import `theme` and `utilities` separately (not the all-in-one `@import 'tailwindcss'`) so Uniwind can inject its layer between them. The `@cdx-ui/components/styles.css` import is critical — without it, components render unstyled because Tailwind never scans the package for `className` usage. It also reverts React Native Web's `display: inline` reset on root `Text` elements, so text aligns inside plain block containers. If your CSS entry has an older `[class*='css-text-']` override, remove it: it only matches development builds and overrides `numberOfLines` and Tailwind display utilities.
427
+
428
+ `@custom-variant dark` enables class-based dark mode for `ScopedTheme` on web. Always import `@cdx-ui/styles/web.css` as a **separate JS import** after your CSS entry (see App entry below) — it carries the web font family values and the unlayered `.dark`/`.light` overrides.
429
+
430
+ ### App entry
431
+
432
+ Import the CSS file as the first line of `src/main.tsx`, then wrap the app with
433
+ `SafeAreaProvider` and `ForgeDesignProvider`:
434
+
435
+ ```tsx
436
+ import './index.css';
437
+ import '@cdx-ui/styles/web.css';
438
+
439
+ import { StrictMode } from 'react';
440
+ import { createRoot } from 'react-dom/client';
441
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
442
+ import { ForgeDesignProvider } from '@cdx-ui/components';
443
+ import App from './App';
444
+
445
+ createRoot(document.getElementById('root')!).render(
446
+ <StrictMode>
447
+ <SafeAreaProvider>
448
+ <ForgeDesignProvider>
449
+ <App />
450
+ </ForgeDesignProvider>
451
+ </SafeAreaProvider>
452
+ </StrictMode>,
453
+ );
454
+ ```
455
+
456
+ `SafeAreaProvider` must remain outside `ForgeDesignProvider`. `ForgeDesignProvider` mounts
457
+ `PortalHost` and `ToastProvider` for you. If you prefer not to use the Forge provider, keep
458
+ `SafeAreaProvider` and mount one `<PortalHost />` from `@cdx-ui/primitives` as a sibling of your app
459
+ (and `ToastProvider` when using toasts). Without a host, `Dialog` / `AlertDialog` fail **silently** —
460
+ state updates, nothing appears, and no error is logged. Do not mount a second host when
461
+ `ForgeDesignProvider` is already in the tree.
462
+
463
+ Select uses React Native Web / native Modal portals and does not require `PortalHost`.
464
+
465
+ ### Runtime shim
466
+
467
+ Add an inline script to `index.html` **before** the module script. React Native dependencies reference `global`, which is undefined in browsers:
468
+
469
+ ```html
470
+ <body>
471
+ <script>window.global = window;</script>
472
+ <div id="root"></div>
473
+ <script type="module" src="/src/main.tsx"></script>
474
+ </body>
475
+ ```
476
+
477
+ This cannot be moved into a module — some references evaluate before your app code runs.
478
+
479
+ ### TypeScript
480
+
481
+ Ensure `tsconfig.json` uses bundler resolution (the Vite React + TS template includes this):
482
+
483
+ ```json
484
+ {
485
+ "compilerOptions": {
486
+ "module": "ESNext",
487
+ "moduleResolution": "bundler",
488
+ "jsx": "react-jsx",
489
+ "strict": true,
490
+ "isolatedModules": true,
491
+ "skipLibCheck": true,
492
+ "lib": ["ES2020", "DOM", "DOM.Iterable"]
493
+ },
494
+ "include": ["src"]
495
+ }
496
+ ```
497
+
498
+ The Uniwind Vite plugin generates `uniwind-types.d.ts` at the project root on first run. Commit this file so CI typecheck does not depend on running the dev server first.
499
+
500
+ ### Verify
501
+
502
+ Render a Forge UI component to confirm the pipeline is wired end to end:
503
+
504
+ ```tsx
505
+ import { Button, Heading, Text, VStack } from '@cdx-ui/components';
506
+
507
+ export default function App() {
508
+ return (
509
+ <VStack space="lg" className="p-6">
510
+ <Heading>Welcome</Heading>
511
+ <Text>Get started with Forge UI.</Text>
512
+ <Button onPress={() => {}}>
513
+ <Button.Label>Continue</Button.Label>
514
+ </Button>
515
+ </VStack>
516
+ );
517
+ }
518
+ ```
519
+
520
+ Then run:
521
+
522
+ ```bash
523
+ npm run dev # http://localhost:5173 should render styled components
524
+ npm run build # must succeed with no Rollup errors
525
+ npm run preview # production bundle should match dev
526
+ ```
527
+
528
+ If the button renders unstyled, check that `@cdx-ui/components/styles.css` is imported in `src/index.css`.
529
+
530
+ ### Testing with Vitest
531
+
532
+ Use Vitest + React Testing Library for consumer-side **web** tests. Native component tests remain Jest + RNTL inside Forge UI packages.
533
+
534
+ Do **not** reuse `forge()` in Vitest. Add a dedicated `vitest.config.ts`:
535
+
536
+ ```ts
537
+ import { defineConfig } from 'vitest/config';
538
+ import react from '@vitejs/plugin-react';
539
+ import { forgeVitest } from '@cdx-ui/web/vite';
540
+
541
+ export default defineConfig({
542
+ plugins: [react(), forgeVitest()],
543
+ test: {
544
+ setupFiles: ['./src/__tests__/setup.ts'],
545
+ },
546
+ });
547
+ ```
548
+
549
+ Install test dependencies (pin exact versions in apps that follow the example lockfile policy):
550
+
551
+ ```bash
552
+ pnpm add vitest @testing-library/react @testing-library/jest-dom jsdom -D
553
+ # or: npm install vitest @testing-library/react @testing-library/jest-dom jsdom -D
554
+ ```
555
+
556
+ ```ts
557
+ // src/__tests__/setup.ts
558
+ import '@testing-library/jest-dom/vitest';
559
+ import { afterEach } from 'vitest';
560
+ import { cleanup } from '@testing-library/react';
561
+
562
+ afterEach(() => {
563
+ cleanup();
564
+ });
565
+ ```
566
+
567
+ Render Forge UI inside `ForgeDesignProvider`. Without its portal host, `Dialog` state can open with no popup in the DOM:
568
+
569
+ ```tsx
570
+ import type { ReactElement } from 'react';
571
+ import { render } from '@testing-library/react';
572
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
573
+ import { ForgeDesignProvider } from '@cdx-ui/components';
574
+
575
+ export function renderWithForgeProviders(ui: ReactElement) {
576
+ return render(ui, {
577
+ wrapper: ({ children }) => (
578
+ <SafeAreaProvider>
579
+ <ForgeDesignProvider config={{ toast: false }}>{children}</ForgeDesignProvider>
580
+ </SafeAreaProvider>
581
+ ),
582
+ });
583
+ }
584
+ ```
585
+
586
+ jsdom is sufficient for static rendering, controlled interactions (Switch, Checkbox), and Dialog portal behavior. It does **not** prove production CSS, layout, or animation fidelity — `forgeVitest()` substitutes Uniwind, Reanimated, Gesture Handler, and related native packages for that reason.
587
+
588
+ A copy-paste harness with representative tests lives in the [Forge UI + MUI Vite example](https://github.com/dfh-candescent/forge-design-system/tree/main/examples/forge-ui-mui-vite).
589
+
590
+ ### Troubleshooting
591
+
592
+ | Symptom | Likely cause |
593
+ | -------------------------------------------------- | --------------------------------------------------------------------------------------------- |
594
+ | Components render unstyled | Missing `@cdx-ui/components/styles.css` import in `src/index.css` |
595
+ | `__DEV__ is not defined` | Missing `define` block in `vite.config.ts` |
596
+ | `global is not defined` | Missing `window.global` shim in `index.html` |
597
+ | Dark mode works in dev but not production | Ensure `forge()` is present before `react()` and its Tailwind configuration is not overridden |
598
+ | Dialog opens with no visible popup | Missing `ForgeDesignProvider` or standalone `PortalHost` |
599
+ | `SyntaxError: Unexpected token 'typeof'` in Vitest | `forge()` is in the Vitest config — use `forgeVitest()` from `@cdx-ui/web/vite` instead |
600
+ | Build fails on `react-native-svg` | Ensure `forge()` is present and its `react-native-svg` alias is not overridden |
601
+ | "Two copies of Reanimated" | Mismatched `@cdx-ui/*` package versions |
602
+
603
+ ---
604
+
605
+ ### Next steps
606
+
607
+ [Components reference](https://design.candescent.com/docs/components/content.md) — detailed docs for every component\
608
+ [Styling and customization](https://design.candescent.com/docs/getting-started/developers/styling/content.md) — what to style, and what to leave to the components\
609
+ [Accessibility](https://design.candescent.com/docs/getting-started/developers/accessibility/content.md) — what the components handle, and what your screen still provides\
610
+ [Icons](https://design.candescent.com/docs/foundations/expressions/icons/content.md) — using `@cdx-ui/icons`\
611
+ [Dialog](https://design.candescent.com/docs/components/dialog/content.md) — overlay setup and portal host requirements\
612
+ [Link / navigation](https://design.candescent.com/docs/components/link/content.md) — router integration with `LinkProvider`