react-native-terra-ui 0.1.1 → 0.2.1

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 (262) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/lib/module/components/avatar/Avatar.js +118 -35
  3. package/lib/module/components/avatar/Avatar.js.map +1 -1
  4. package/lib/module/components/box/Box.js +15 -2
  5. package/lib/module/components/box/Box.js.map +1 -1
  6. package/lib/module/components/box/utils.js +4 -1
  7. package/lib/module/components/box/utils.js.map +1 -1
  8. package/lib/module/components/button/Button.js +7 -14
  9. package/lib/module/components/button/Button.js.map +1 -1
  10. package/lib/module/components/chip/Chip.js +372 -0
  11. package/lib/module/components/chip/Chip.js.map +1 -0
  12. package/lib/module/components/chip/index.js +4 -0
  13. package/lib/module/components/chip/index.js.map +1 -0
  14. package/lib/module/components/chip/types.js +2 -0
  15. package/lib/module/components/chip/types.js.map +1 -0
  16. package/lib/module/components/chip/utils.js +36 -0
  17. package/lib/module/components/chip/utils.js.map +1 -0
  18. package/lib/module/components/empty-state/EmptyState.js +200 -0
  19. package/lib/module/components/empty-state/EmptyState.js.map +1 -0
  20. package/lib/module/components/empty-state/index.js +4 -0
  21. package/lib/module/components/empty-state/index.js.map +1 -0
  22. package/lib/module/components/empty-state/types.js +2 -0
  23. package/lib/module/components/empty-state/types.js.map +1 -0
  24. package/lib/module/components/header/header-buttons.js +31 -36
  25. package/lib/module/components/header/header-buttons.js.map +1 -1
  26. package/lib/module/components/header/index.js +1 -1
  27. package/lib/module/components/header/index.js.map +1 -1
  28. package/lib/module/components/header/parts/HeaderGradientOverlay.js +38 -0
  29. package/lib/module/components/header/parts/HeaderGradientOverlay.js.map +1 -0
  30. package/lib/module/components/header/variants/LargeTitleHeader/index.js +69 -99
  31. package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
  32. package/lib/module/components/header/variants/TitleHeader/index.js +32 -63
  33. package/lib/module/components/header/variants/TitleHeader/index.js.map +1 -1
  34. package/lib/module/components/icon/Icon.js +20 -8
  35. package/lib/module/components/icon/Icon.js.map +1 -1
  36. package/lib/module/components/image/Image.js +54 -0
  37. package/lib/module/components/image/Image.js.map +1 -0
  38. package/lib/module/components/image/index.js +4 -0
  39. package/lib/module/components/image/index.js.map +1 -0
  40. package/lib/module/components/image/types.js +4 -0
  41. package/lib/module/components/image/types.js.map +1 -0
  42. package/lib/module/components/index.js +4 -0
  43. package/lib/module/components/index.js.map +1 -1
  44. package/lib/module/components/portal/Portal.js +42 -0
  45. package/lib/module/components/portal/Portal.js.map +1 -0
  46. package/lib/module/components/portal/PortalContext.js +1 -16
  47. package/lib/module/components/portal/PortalContext.js.map +1 -1
  48. package/lib/module/components/portal/PortalHost.js +16 -14
  49. package/lib/module/components/portal/PortalHost.js.map +1 -1
  50. package/lib/module/components/portal/PortalProvider.js +51 -19
  51. package/lib/module/components/portal/PortalProvider.js.map +1 -1
  52. package/lib/module/components/portal/index.js +1 -1
  53. package/lib/module/components/portal/index.js.map +1 -1
  54. package/lib/module/components/screen/Screen.js +25 -28
  55. package/lib/module/components/screen/Screen.js.map +1 -1
  56. package/lib/module/components/screen/ScreenContext.js +7 -5
  57. package/lib/module/components/screen/ScreenContext.js.map +1 -1
  58. package/lib/module/components/screen/ScreenFlashList.js +144 -0
  59. package/lib/module/components/screen/ScreenFlashList.js.map +1 -0
  60. package/lib/module/components/screen/ScreenFlatList.js +88 -27
  61. package/lib/module/components/screen/ScreenFlatList.js.map +1 -1
  62. package/lib/module/components/screen/ScreenScrollView.js +101 -34
  63. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  64. package/lib/module/components/screen/index.js +1 -0
  65. package/lib/module/components/screen/index.js.map +1 -1
  66. package/lib/module/components/screen/parts/BottomSafeArea.js +62 -0
  67. package/lib/module/components/screen/parts/BottomSafeArea.js.map +1 -0
  68. package/lib/module/components/screen/parts/ScreenPlaceholder.js +50 -0
  69. package/lib/module/components/screen/parts/ScreenPlaceholder.js.map +1 -0
  70. package/lib/module/components/screen/types.js +4 -0
  71. package/lib/module/components/screen/types.js.map +1 -0
  72. package/lib/module/components/screen/utils.js +12 -9
  73. package/lib/module/components/screen/utils.js.map +1 -1
  74. package/lib/module/components/surface/Surface.js +16 -9
  75. package/lib/module/components/surface/Surface.js.map +1 -1
  76. package/lib/module/components/text/Text.js +23 -5
  77. package/lib/module/components/text/Text.js.map +1 -1
  78. package/lib/module/components/toast/Toast.js +8 -8
  79. package/lib/module/components/toast/Toast.js.map +1 -1
  80. package/lib/module/components/toolbar/Toolbar.js +268 -0
  81. package/lib/module/components/toolbar/Toolbar.js.map +1 -0
  82. package/lib/module/components/toolbar/index.js +4 -0
  83. package/lib/module/components/toolbar/index.js.map +1 -0
  84. package/lib/module/context/ThemeProvider.js +5 -2
  85. package/lib/module/context/ThemeProvider.js.map +1 -1
  86. package/lib/module/{components/icon/utils.js → theme/defaults/icons.js} +1 -1
  87. package/lib/module/theme/defaults/icons.js.map +1 -0
  88. package/lib/module/theme/defaults/image.js +30 -0
  89. package/lib/module/theme/defaults/image.js.map +1 -0
  90. package/lib/module/theme/index.js +1 -1
  91. package/lib/module/theme/{configure.js → registry.js} +21 -28
  92. package/lib/module/theme/registry.js.map +1 -0
  93. package/lib/module/theme/runtime.js +1 -1
  94. package/lib/module/theme/theme.js +91 -10
  95. package/lib/module/theme/theme.js.map +1 -1
  96. package/lib/module/theme/tokens/dark.js +59 -68
  97. package/lib/module/theme/tokens/dark.js.map +1 -1
  98. package/lib/module/theme/tokens/light.js +49 -67
  99. package/lib/module/theme/tokens/light.js.map +1 -1
  100. package/lib/module/theme/tokens/primitives.js +241 -20
  101. package/lib/module/theme/tokens/primitives.js.map +1 -1
  102. package/lib/module/utils/accent-utils.js +4 -4
  103. package/lib/module/utils/accent-utils.js.map +1 -1
  104. package/lib/typescript/src/components/avatar/Avatar.d.ts.map +1 -1
  105. package/lib/typescript/src/components/avatar/types.d.ts +4 -4
  106. package/lib/typescript/src/components/avatar/types.d.ts.map +1 -1
  107. package/lib/typescript/src/components/box/Box.d.ts.map +1 -1
  108. package/lib/typescript/src/components/box/utils.d.ts +6 -1
  109. package/lib/typescript/src/components/box/utils.d.ts.map +1 -1
  110. package/lib/typescript/src/components/button/Button.d.ts +1 -1
  111. package/lib/typescript/src/components/button/Button.d.ts.map +1 -1
  112. package/lib/typescript/src/components/chip/Chip.d.ts +24 -0
  113. package/lib/typescript/src/components/chip/Chip.d.ts.map +1 -0
  114. package/lib/typescript/src/components/chip/index.d.ts +4 -0
  115. package/lib/typescript/src/components/chip/index.d.ts.map +1 -0
  116. package/lib/typescript/src/components/chip/types.d.ts +8 -0
  117. package/lib/typescript/src/components/chip/types.d.ts.map +1 -0
  118. package/lib/typescript/src/components/chip/utils.d.ts +8 -0
  119. package/lib/typescript/src/components/chip/utils.d.ts.map +1 -0
  120. package/lib/typescript/src/components/empty-state/EmptyState.d.ts +45 -0
  121. package/lib/typescript/src/components/empty-state/EmptyState.d.ts.map +1 -0
  122. package/lib/typescript/src/components/empty-state/index.d.ts +4 -0
  123. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -0
  124. package/lib/typescript/src/components/empty-state/types.d.ts +6 -0
  125. package/lib/typescript/src/components/empty-state/types.d.ts.map +1 -0
  126. package/lib/typescript/src/components/header/header-buttons.d.ts +27 -0
  127. package/lib/typescript/src/components/header/header-buttons.d.ts.map +1 -1
  128. package/lib/typescript/src/components/header/index.d.ts +1 -1
  129. package/lib/typescript/src/components/header/index.d.ts.map +1 -1
  130. package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts +7 -0
  131. package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts.map +1 -0
  132. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts +1 -1
  133. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
  134. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts +18 -7
  135. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts.map +1 -1
  136. package/lib/typescript/src/components/icon/Icon.d.ts.map +1 -1
  137. package/lib/typescript/src/components/image/Image.d.ts +16 -0
  138. package/lib/typescript/src/components/image/Image.d.ts.map +1 -0
  139. package/lib/typescript/src/components/image/index.d.ts +3 -0
  140. package/lib/typescript/src/components/image/index.d.ts.map +1 -0
  141. package/lib/typescript/src/components/image/types.d.ts +7 -0
  142. package/lib/typescript/src/components/image/types.d.ts.map +1 -0
  143. package/lib/typescript/src/components/index.d.ts +4 -0
  144. package/lib/typescript/src/components/index.d.ts.map +1 -1
  145. package/lib/typescript/src/components/portal/Portal.d.ts +26 -0
  146. package/lib/typescript/src/components/portal/Portal.d.ts.map +1 -0
  147. package/lib/typescript/src/components/portal/PortalContext.d.ts +11 -14
  148. package/lib/typescript/src/components/portal/PortalContext.d.ts.map +1 -1
  149. package/lib/typescript/src/components/portal/PortalHost.d.ts +9 -4
  150. package/lib/typescript/src/components/portal/PortalHost.d.ts.map +1 -1
  151. package/lib/typescript/src/components/portal/PortalProvider.d.ts +3 -3
  152. package/lib/typescript/src/components/portal/PortalProvider.d.ts.map +1 -1
  153. package/lib/typescript/src/components/portal/index.d.ts +4 -2
  154. package/lib/typescript/src/components/portal/index.d.ts.map +1 -1
  155. package/lib/typescript/src/components/screen/Screen.d.ts +18 -13
  156. package/lib/typescript/src/components/screen/Screen.d.ts.map +1 -1
  157. package/lib/typescript/src/components/screen/ScreenContext.d.ts +12 -9
  158. package/lib/typescript/src/components/screen/ScreenContext.d.ts.map +1 -1
  159. package/lib/typescript/src/components/screen/ScreenFlashList.d.ts +43 -0
  160. package/lib/typescript/src/components/screen/ScreenFlashList.d.ts.map +1 -0
  161. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts +17 -7
  162. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts.map +1 -1
  163. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts +51 -16
  164. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
  165. package/lib/typescript/src/components/screen/index.d.ts +3 -0
  166. package/lib/typescript/src/components/screen/index.d.ts.map +1 -1
  167. package/lib/typescript/src/components/screen/parts/BottomSafeArea.d.ts +30 -0
  168. package/lib/typescript/src/components/screen/parts/BottomSafeArea.d.ts.map +1 -0
  169. package/lib/typescript/src/components/screen/parts/ScreenPlaceholder.d.ts +22 -0
  170. package/lib/typescript/src/components/screen/parts/ScreenPlaceholder.d.ts.map +1 -0
  171. package/lib/typescript/src/components/screen/types.d.ts +42 -0
  172. package/lib/typescript/src/components/screen/types.d.ts.map +1 -0
  173. package/lib/typescript/src/components/screen/utils.d.ts +9 -9
  174. package/lib/typescript/src/components/screen/utils.d.ts.map +1 -1
  175. package/lib/typescript/src/components/surface/Surface.d.ts +7 -0
  176. package/lib/typescript/src/components/surface/Surface.d.ts.map +1 -1
  177. package/lib/typescript/src/components/text/Text.d.ts.map +1 -1
  178. package/lib/typescript/src/components/toolbar/Toolbar.d.ts +115 -0
  179. package/lib/typescript/src/components/toolbar/Toolbar.d.ts.map +1 -0
  180. package/lib/typescript/src/components/toolbar/index.d.ts +3 -0
  181. package/lib/typescript/src/components/toolbar/index.d.ts.map +1 -0
  182. package/lib/typescript/src/context/ThemeProvider.d.ts.map +1 -1
  183. package/lib/typescript/src/{components/icon/utils.d.ts → theme/defaults/icons.d.ts} +2 -2
  184. package/lib/typescript/src/theme/defaults/icons.d.ts.map +1 -0
  185. package/lib/typescript/src/theme/defaults/image.d.ts +4 -0
  186. package/lib/typescript/src/theme/defaults/image.d.ts.map +1 -0
  187. package/lib/typescript/src/theme/index.d.ts +1 -1
  188. package/lib/typescript/src/theme/index.d.ts.map +1 -1
  189. package/lib/typescript/src/theme/{configure.d.ts → registry.d.ts} +1 -1
  190. package/lib/typescript/src/theme/registry.d.ts.map +1 -0
  191. package/lib/typescript/src/theme/theme.d.ts +21 -1
  192. package/lib/typescript/src/theme/theme.d.ts.map +1 -1
  193. package/lib/typescript/src/theme/tokens/dark.d.ts +41 -64
  194. package/lib/typescript/src/theme/tokens/dark.d.ts.map +1 -1
  195. package/lib/typescript/src/theme/tokens/light.d.ts +44 -65
  196. package/lib/typescript/src/theme/tokens/light.d.ts.map +1 -1
  197. package/lib/typescript/src/theme/tokens/primitives.d.ts +228 -17
  198. package/lib/typescript/src/theme/tokens/primitives.d.ts.map +1 -1
  199. package/lib/typescript/src/theme/types.d.ts +65 -35
  200. package/lib/typescript/src/theme/types.d.ts.map +1 -1
  201. package/package.json +24 -12
  202. package/skills/terra-ui/SKILL.md +193 -0
  203. package/skills/terra-ui/references/recipes.md +198 -0
  204. package/skills/terra-ui/references/troubleshooting.md +112 -0
  205. package/src/components/avatar/Avatar.tsx +128 -38
  206. package/src/components/avatar/types.ts +5 -5
  207. package/src/components/box/Box.tsx +13 -1
  208. package/src/components/box/utils.ts +10 -0
  209. package/src/components/button/Button.tsx +7 -15
  210. package/src/components/chip/Chip.tsx +444 -0
  211. package/src/components/chip/index.ts +3 -0
  212. package/src/components/chip/types.ts +15 -0
  213. package/src/components/chip/utils.ts +34 -0
  214. package/src/components/empty-state/EmptyState.tsx +209 -0
  215. package/src/components/empty-state/index.ts +3 -0
  216. package/src/components/empty-state/types.ts +5 -0
  217. package/src/components/header/header-buttons.tsx +33 -45
  218. package/src/components/header/index.ts +2 -0
  219. package/src/components/header/parts/HeaderGradientOverlay.tsx +29 -0
  220. package/src/components/header/variants/LargeTitleHeader/index.tsx +68 -102
  221. package/src/components/header/variants/TitleHeader/index.tsx +47 -56
  222. package/src/components/icon/Icon.tsx +20 -15
  223. package/src/components/image/Image.tsx +48 -0
  224. package/src/components/image/index.ts +2 -0
  225. package/src/components/image/types.ts +7 -0
  226. package/src/components/index.ts +4 -0
  227. package/src/components/portal/Portal.tsx +55 -0
  228. package/src/components/portal/PortalContext.tsx +14 -22
  229. package/src/components/portal/PortalHost.tsx +17 -10
  230. package/src/components/portal/PortalProvider.tsx +58 -21
  231. package/src/components/portal/index.ts +4 -2
  232. package/src/components/screen/Screen.tsx +45 -59
  233. package/src/components/screen/ScreenContext.tsx +19 -12
  234. package/src/components/screen/ScreenFlashList.tsx +196 -0
  235. package/src/components/screen/ScreenFlatList.tsx +100 -39
  236. package/src/components/screen/ScreenScrollView.tsx +114 -53
  237. package/src/components/screen/index.ts +3 -0
  238. package/src/components/screen/parts/BottomSafeArea.tsx +60 -0
  239. package/src/components/screen/parts/ScreenPlaceholder.tsx +55 -0
  240. package/src/components/screen/types.ts +43 -0
  241. package/src/components/screen/utils.ts +29 -15
  242. package/src/components/surface/Surface.tsx +17 -20
  243. package/src/components/text/Text.tsx +23 -5
  244. package/src/components/toast/Toast.tsx +8 -8
  245. package/src/components/toolbar/Toolbar.tsx +353 -0
  246. package/src/components/toolbar/index.ts +6 -0
  247. package/src/context/ThemeProvider.tsx +5 -4
  248. package/src/{components/icon/utils.tsx → theme/defaults/icons.tsx} +1 -1
  249. package/src/theme/defaults/image.tsx +34 -0
  250. package/src/theme/index.ts +1 -1
  251. package/src/theme/{configure.ts → registry.ts} +19 -34
  252. package/src/theme/runtime.ts +1 -1
  253. package/src/theme/theme.ts +127 -13
  254. package/src/theme/tokens/dark.ts +60 -69
  255. package/src/theme/tokens/light.ts +50 -68
  256. package/src/theme/tokens/primitives.ts +248 -20
  257. package/src/theme/types.ts +67 -35
  258. package/src/utils/accent-utils.ts +4 -4
  259. package/lib/module/components/icon/utils.js.map +0 -1
  260. package/lib/module/theme/configure.js.map +0 -1
  261. package/lib/typescript/src/components/icon/utils.d.ts.map +0 -1
  262. package/lib/typescript/src/theme/configure.d.ts.map +0 -1
@@ -0,0 +1,444 @@
1
+ import {
2
+ Children,
3
+ type ComponentRef,
4
+ createContext,
5
+ forwardRef,
6
+ type ReactNode,
7
+ useContext,
8
+ } from 'react';
9
+ import { View, type ViewProps } from 'react-native';
10
+
11
+ import { StyleSheet, withUnistyles } from 'react-native-unistyles';
12
+
13
+ import type { ColorToken, TerraIconName, TextVariant } from '#theme/types';
14
+
15
+ import { Icon } from '../icon';
16
+ import { Text } from '../text';
17
+ import type { ChipColor, ChipSize, ChipVariant } from './types';
18
+ import { type CustomChipColors, getCustomColorColors } from './utils';
19
+
20
+ export interface ChipProps extends ViewProps {
21
+ size?: ChipSize;
22
+ variant?: ChipVariant;
23
+ color?: ChipColor;
24
+ children?: ReactNode;
25
+ }
26
+
27
+ // ─── Context (lets Chip.Icon read the resolved foreground + size) ─────────────
28
+
29
+ interface ChipContextValue {
30
+ /**
31
+ * Foreground as a **token** (or a raw literal for a custom color), never a
32
+ * resolved value — `Text` and `Icon` resolve it against the live theme, so
33
+ * each owns its own subscription and the label keeps working however the
34
+ * chip above it is rendered.
35
+ */
36
+ color: ColorToken;
37
+ size: ChipSize;
38
+ }
39
+
40
+ const ChipContext = createContext<ChipContextValue>({
41
+ color: 'text.default',
42
+ size: 'md',
43
+ });
44
+
45
+ // ─── Colors ─────────────────────────────────────────────────────────────────
46
+ //
47
+ // The chip's own paint (background + border) is a style, so named schemes
48
+ // (primary/secondary/success/warning/danger) are resolved declaratively via
49
+ // unistyles `compoundVariants` on `styles.base` — one entry per (color, variant)
50
+ // pair, straight from the spec's token table.
51
+ //
52
+ // The foreground is *not* a style on this View — it is inherited by the label
53
+ // and icon. It therefore lives here as a token table and is passed down
54
+ // unresolved, so `Text`/`Icon` resolve it against the live theme on every
55
+ // change.
56
+ //
57
+ // A custom color literal (hex/rgb/hsl) isn't a bounded set of options, so it
58
+ // can't be a compound variant condition — see `getCustomColorColors` in
59
+ // `utils.ts` for how it's derived instead.
60
+
61
+ const SCHEME_NAMES = new Set([
62
+ 'primary',
63
+ 'secondary',
64
+ 'success',
65
+ 'warning',
66
+ 'danger',
67
+ ]);
68
+
69
+ type ChipScheme = 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
70
+ type ChipPaintedVariant = Exclude<ChipVariant, 'ghost'>;
71
+
72
+ /** Foreground token per (scheme, variant) — mirrors the spec's token table. */
73
+ const FG_TOKEN: Record<ChipScheme, Record<ChipPaintedVariant, ColorToken>> = {
74
+ primary: {
75
+ solid: 'action.fg.primary',
76
+ soft: 'action.fg.primary.subtle',
77
+ outline: 'text.accent',
78
+ },
79
+ secondary: {
80
+ solid: 'action.fg.secondary',
81
+ soft: 'action.fg.secondary.subtle',
82
+ outline: 'text.default',
83
+ },
84
+ success: {
85
+ solid: 'status.success.fg',
86
+ soft: 'status.success.fg.subtle',
87
+ outline: 'status.success.fg.subtle',
88
+ },
89
+ warning: {
90
+ solid: 'status.warning.fg',
91
+ soft: 'status.warning.fg.subtle',
92
+ outline: 'status.warning.fg.subtle',
93
+ },
94
+ danger: {
95
+ solid: 'status.danger.fg',
96
+ soft: 'status.danger.fg.subtle',
97
+ outline: 'status.danger.fg.subtle',
98
+ },
99
+ };
100
+
101
+ /** The label/icon color as a token, so children resolve it per theme. */
102
+ function resolveForeground(
103
+ color: ChipColor,
104
+ variant: ChipVariant,
105
+ customColors: CustomChipColors | null
106
+ ): ColorToken {
107
+ if (customColors) return customColors.fg;
108
+ if (variant === 'ghost') return 'text.default';
109
+ return FG_TOKEN[color as ChipScheme][variant];
110
+ }
111
+
112
+ // ─── Label sub-component ───────────────────────────────────────────────────────
113
+
114
+ const LABEL_VARIANT: Record<ChipSize, TextVariant> = {
115
+ sm: 'label-sm',
116
+ md: 'label-md',
117
+ lg: 'label-lg',
118
+ };
119
+
120
+ const ChipLabel = ({ children }: { children: ReactNode }) => {
121
+ const { color, size } = useContext(ChipContext);
122
+ return (
123
+ <Text variant={LABEL_VARIANT[size]} weight="semibold" color={color}>
124
+ {children}
125
+ </Text>
126
+ );
127
+ };
128
+
129
+ // ─── Icon sub-component ────────────────────────────────────────────────────────
130
+
131
+ const ICON_SIZE: Record<ChipSize, number> = {
132
+ sm: 12,
133
+ md: 14,
134
+ lg: 16,
135
+ };
136
+
137
+ export interface ChipIconProps extends Omit<ViewProps, 'children'> {
138
+ name: TerraIconName;
139
+ size?: number;
140
+ color?: ColorToken;
141
+ strokeWidth?: number;
142
+ }
143
+
144
+ const ChipIcon = ({
145
+ name,
146
+ size: sizeProp,
147
+ color,
148
+ strokeWidth,
149
+ style,
150
+ ...rest
151
+ }: ChipIconProps) => {
152
+ const { color: contextColor, size } = useContext(ChipContext);
153
+
154
+ return (
155
+ <Icon
156
+ name={name}
157
+ color={color || contextColor}
158
+ size={sizeProp ?? ICON_SIZE[size]}
159
+ strokeWidth={strokeWidth}
160
+ style={style}
161
+ {...rest}
162
+ />
163
+ );
164
+ };
165
+
166
+ function renderChildren(children: ReactNode): ReactNode {
167
+ return Children.map(children, (child) => {
168
+ if (typeof child === 'string') {
169
+ return child.trim().length > 0 ? <ChipLabel>{child}</ChipLabel> : null;
170
+ }
171
+ if (typeof child === 'number') {
172
+ return <ChipLabel>{child}</ChipLabel>;
173
+ }
174
+ return child;
175
+ });
176
+ }
177
+
178
+ // ─── Component ────────────────────────────────────────────────────────────────
179
+
180
+ const ChipRoot = forwardRef<ComponentRef<typeof View>, ChipProps>(function Chip(
181
+ {
182
+ size = 'md',
183
+ variant = 'soft',
184
+ color = 'secondary',
185
+ children,
186
+ style,
187
+ ...rest
188
+ },
189
+ ref
190
+ ) {
191
+ // Compound variants only match the 5 named schemes above — an arbitrary
192
+ // literal never matches one, so it's cast to satisfy `useVariants`'s
193
+ // literal-union type while still comparing against the real value below.
194
+ styles.useVariants({ color: color as ChipScheme, variant, size });
195
+
196
+ const isCustomColor = variant !== 'ghost' && !SCHEME_NAMES.has(color);
197
+ const customColors = isCustomColor
198
+ ? getCustomColorColors(color, variant as 'solid' | 'soft' | 'outline')
199
+ : null;
200
+
201
+ const fg = resolveForeground(color, variant, customColors);
202
+
203
+ return (
204
+ <ChipContext.Provider value={{ color: fg, size }}>
205
+ <View
206
+ ref={ref}
207
+ style={[
208
+ styles.base,
209
+ customColors
210
+ ? {
211
+ backgroundColor: customColors.bg,
212
+ borderColor: customColors.border,
213
+ borderWidth: customColors.borderWidth,
214
+ }
215
+ : null,
216
+ style,
217
+ ]}
218
+ {...rest}
219
+ >
220
+ {renderChildren(children)}
221
+ </View>
222
+ </ChipContext.Provider>
223
+ );
224
+ });
225
+
226
+ type ChipComponent = typeof ChipRoot & {
227
+ Icon: typeof ChipIcon;
228
+ };
229
+
230
+ // Subscribes the chip to theme changes. The background below comes from
231
+ // `compoundVariants`, which unistyles applies straight to the native node when
232
+ // the theme switches — no re-render, so React goes on believing the node holds
233
+ // the previous theme's value. React Native only mutates a node when newly
234
+ // rendered props differ from the last ones it rendered, so a later variant
235
+ // change that computes a value equal to that stale record emits no update at
236
+ // all, and the node keeps whatever the theme switch painted. That is reachable
237
+ // whenever one colour fills two roles across themes — an accent equal to
238
+ // another theme's `action.bg.secondary.subtle`, say — and it shows up as a
239
+ // deselected chip still wearing the selected background.
240
+ //
241
+ // Re-rendering on a theme switch commits the new value there and then, so no
242
+ // stale record survives to collide. `Text` already subscribes this way for its
243
+ // colour; this gives the chip's own paint the same guarantee. The cast keeps
244
+ // `ChipProps` and the ref, which `withUnistyles` widens to `Partial<{}>`.
245
+ export const Chip = withUnistyles(ChipRoot) as ChipComponent;
246
+ Chip.Icon = ChipIcon;
247
+
248
+ // ─── Styles ───────────────────────────────────────────────────────────────────
249
+
250
+ // One compound variant per (color, variant) cell of the spec's token table —
251
+ // `variants` below only registers the axis names/options for the type
252
+ // system; the actual bg/border values all come from `compoundVariants`.
253
+ // `size` has no cross-axis interaction, so it's a plain variant.
254
+ //
255
+ // Foreground is deliberately absent: it belongs to the label and icon, not to
256
+ // this View, and is handed to them as a token via `FG_TOKEN` / context.
257
+ const styles = StyleSheet.create((theme) => {
258
+ const c = theme.color;
259
+
260
+ return {
261
+ base: {
262
+ flexDirection: 'row',
263
+ alignItems: 'center',
264
+ alignSelf: 'flex-start',
265
+ borderRadius: theme.radius.full,
266
+ variants: {
267
+ size: {
268
+ sm: {
269
+ minHeight: 24,
270
+ paddingVertical: 6,
271
+ paddingHorizontal: 12,
272
+ gap: 4,
273
+ },
274
+ md: {
275
+ minHeight: 26,
276
+ paddingVertical: 8,
277
+ paddingHorizontal: 14,
278
+ gap: 4,
279
+ },
280
+ lg: {
281
+ minHeight: 28,
282
+ paddingVertical: 8,
283
+ paddingHorizontal: 16,
284
+ gap: 4,
285
+ },
286
+ },
287
+ color: {
288
+ primary: {},
289
+ secondary: {},
290
+ success: {},
291
+ warning: {},
292
+ danger: {},
293
+ },
294
+ variant: {
295
+ solid: {},
296
+ soft: {},
297
+ outline: {},
298
+ ghost: {
299
+ backgroundColor: 'transparent',
300
+ borderColor: 'transparent',
301
+ borderWidth: 0,
302
+ },
303
+ },
304
+ },
305
+ compoundVariants: [
306
+ {
307
+ color: 'primary',
308
+ variant: 'solid',
309
+ styles: {
310
+ backgroundColor: c['action.bg.primary'],
311
+ borderColor: c['action.bg.primary'],
312
+ borderWidth: 0,
313
+ },
314
+ },
315
+ {
316
+ color: 'primary',
317
+ variant: 'soft',
318
+ styles: {
319
+ backgroundColor: c['action.bg.primary.subtle'],
320
+ borderColor: c['action.bg.primary.subtle'],
321
+ borderWidth: 0,
322
+ },
323
+ },
324
+ {
325
+ color: 'primary',
326
+ variant: 'outline',
327
+ styles: {
328
+ backgroundColor: 'transparent',
329
+ borderColor: c['border.accent'],
330
+ borderWidth: 1,
331
+ },
332
+ },
333
+ {
334
+ color: 'secondary',
335
+ variant: 'solid',
336
+ styles: {
337
+ backgroundColor: c['action.bg.secondary'],
338
+ borderColor: c['action.bg.secondary'],
339
+ borderWidth: 0,
340
+ },
341
+ },
342
+ {
343
+ color: 'secondary',
344
+ variant: 'soft',
345
+ styles: {
346
+ backgroundColor: c['action.bg.secondary.subtle'],
347
+ borderColor: c['action.bg.secondary.subtle'],
348
+ borderWidth: 0,
349
+ },
350
+ },
351
+ {
352
+ color: 'secondary',
353
+ variant: 'outline',
354
+ styles: {
355
+ backgroundColor: 'transparent',
356
+ borderColor: c['border.default'],
357
+ borderWidth: 1,
358
+ },
359
+ },
360
+ {
361
+ color: 'success',
362
+ variant: 'solid',
363
+ styles: {
364
+ backgroundColor: c['status.success.bg'],
365
+ borderColor: c['status.success.bg'],
366
+ borderWidth: 0,
367
+ },
368
+ },
369
+ {
370
+ color: 'success',
371
+ variant: 'soft',
372
+ styles: {
373
+ backgroundColor: c['status.success.bg.subtle'],
374
+ borderColor: c['status.success.bg.subtle'],
375
+ borderWidth: 0,
376
+ },
377
+ },
378
+ {
379
+ color: 'success',
380
+ variant: 'outline',
381
+ styles: {
382
+ backgroundColor: 'transparent',
383
+ borderColor: c['status.success.border'],
384
+ borderWidth: 1,
385
+ },
386
+ },
387
+ {
388
+ color: 'warning',
389
+ variant: 'solid',
390
+ styles: {
391
+ backgroundColor: c['status.warning.bg'],
392
+ borderColor: c['status.warning.bg'],
393
+ borderWidth: 0,
394
+ },
395
+ },
396
+ {
397
+ color: 'warning',
398
+ variant: 'soft',
399
+ styles: {
400
+ backgroundColor: c['status.warning.bg.subtle'],
401
+ borderColor: c['status.warning.bg.subtle'],
402
+ borderWidth: 0,
403
+ },
404
+ },
405
+ {
406
+ color: 'warning',
407
+ variant: 'outline',
408
+ styles: {
409
+ backgroundColor: 'transparent',
410
+ borderColor: c['status.warning.border'],
411
+ borderWidth: 1,
412
+ },
413
+ },
414
+ {
415
+ color: 'danger',
416
+ variant: 'solid',
417
+ styles: {
418
+ backgroundColor: c['status.danger.bg'],
419
+ borderColor: c['status.danger.bg'],
420
+ borderWidth: 0,
421
+ },
422
+ },
423
+ {
424
+ color: 'danger',
425
+ variant: 'soft',
426
+ styles: {
427
+ backgroundColor: c['status.danger.bg.subtle'],
428
+ borderColor: c['status.danger.bg.subtle'],
429
+ borderWidth: 0,
430
+ },
431
+ },
432
+ {
433
+ color: 'danger',
434
+ variant: 'outline',
435
+ styles: {
436
+ backgroundColor: 'transparent',
437
+ borderColor: c['status.danger.border'],
438
+ borderWidth: 1,
439
+ },
440
+ },
441
+ ],
442
+ },
443
+ };
444
+ });
@@ -0,0 +1,3 @@
1
+ export type { ChipIconProps, ChipProps } from './Chip';
2
+ export { Chip } from './Chip';
3
+ export type { ChipColor, ChipSize, ChipVariant } from './types';
@@ -0,0 +1,15 @@
1
+ export type ChipSize = 'sm' | 'md' | 'lg';
2
+ export type ChipVariant = 'solid' | 'soft' | 'outline' | 'ghost';
3
+
4
+ /**
5
+ * Semantic color scheme, or a raw CSS color literal (`#fff`, `rgb(...)`, `hsl(...)`).
6
+ * Has no effect when `variant="ghost"` — ghost always renders `text.default`.
7
+ */
8
+ export type ChipColor =
9
+ | 'primary'
10
+ | 'secondary'
11
+ | 'success'
12
+ | 'warning'
13
+ | 'danger'
14
+ // literals last so scheme-name autocomplete still surfaces
15
+ | (string & {});
@@ -0,0 +1,34 @@
1
+ import { readableOn, withAlpha } from '#utils/color-utils';
2
+
3
+ // A custom color literal (hex/rgb/hsl) can't be a unistyles compound variant
4
+ // condition (it's not a bounded set of options), so it's derived here
5
+ // instead, the same way `utils/accent-utils.ts` expands a single hue into a
6
+ // full color scheme.
7
+
8
+ export interface CustomChipColors {
9
+ bg: string;
10
+ border: string;
11
+ borderWidth: number;
12
+ fg: string;
13
+ }
14
+
15
+ const CUSTOM_COLOR_SOFT_ALPHA = 0.16;
16
+
17
+ export function getCustomColorColors(
18
+ literal: string,
19
+ variant: 'solid' | 'soft' | 'outline'
20
+ ): CustomChipColors {
21
+ if (variant === 'outline') {
22
+ return { bg: 'transparent', border: literal, borderWidth: 1, fg: literal };
23
+ }
24
+ if (variant === 'soft') {
25
+ const bg = withAlpha(literal, CUSTOM_COLOR_SOFT_ALPHA);
26
+ return { bg, border: bg, borderWidth: 0, fg: literal };
27
+ }
28
+ return {
29
+ bg: literal,
30
+ border: literal,
31
+ borderWidth: 0,
32
+ fg: readableOn(literal),
33
+ };
34
+ }
@@ -0,0 +1,209 @@
1
+ import { type ComponentRef, forwardRef, type ReactNode } from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+
4
+ import { StyleSheet } from 'react-native-unistyles';
5
+
6
+ import type { ColorToken, TerraIconName, TextVariant } from '#theme/types';
7
+
8
+ import { Icon } from '../icon';
9
+ import { Text } from '../text';
10
+ import type { EmptyStateSize } from './types';
11
+
12
+ export interface EmptyStateProps extends Omit<ViewProps, 'children'> {
13
+ /** Icon rendered in the circular badge above the title. */
14
+ icon?: TerraIconName;
15
+ /** Icon tint. Defaults to `text.subtle`. */
16
+ iconColor?: ColorToken;
17
+ /**
18
+ * Replaces the icon badge entirely — an illustration, `Image`, or any custom
19
+ * node. Takes precedence over `icon`.
20
+ */
21
+ media?: ReactNode;
22
+ /** Headline. A bare string is wrapped in the size's title `Text`. */
23
+ title: ReactNode;
24
+ /** Supporting copy under the title. A bare string gets a muted body `Text`. */
25
+ description?: ReactNode;
26
+ /** Call to action — typically a `Button`, or a fragment of two. */
27
+ action?: ReactNode;
28
+ size?: EmptyStateSize;
29
+ /**
30
+ * Expand to fill the parent (`flex: 1` plus cross-axis stretch) so the
31
+ * content sits in the middle of it rather than hugging its own height.
32
+ * Inside a `ScrollView`, put the flex on `contentContainerStyle` instead —
33
+ * a scroll view's children cannot flex against its content size.
34
+ */
35
+ fill?: boolean;
36
+ }
37
+
38
+ // ─── Per-size lookups ─────────────────────────────────────────────────────────
39
+ //
40
+ // These feed *props* (a `Text` variant, an `Icon` size in dp), not styles, so
41
+ // they stay plain tables here; everything that is a style is a unistyles
42
+ // variant below.
43
+
44
+ const TYPOGRAPHY: Record<
45
+ EmptyStateSize,
46
+ { title: TextVariant; description: TextVariant }
47
+ > = {
48
+ sm: { title: 'title-sm', description: 'body-sm' },
49
+ md: { title: 'title-md', description: 'body-md' },
50
+ lg: { title: 'title-lg', description: 'body-lg' },
51
+ };
52
+
53
+ const ICON_SIZE: Record<EmptyStateSize, number> = {
54
+ sm: 28,
55
+ md: 34,
56
+ lg: 44,
57
+ };
58
+
59
+ /** Wraps a bare string/number in the slot's themed `Text`; elements pass through. */
60
+ function renderSlot(
61
+ node: ReactNode,
62
+ wrap: (value: ReactNode) => ReactNode
63
+ ): ReactNode {
64
+ if (node === null || node === undefined || node === false) return null;
65
+ return typeof node === 'string' || typeof node === 'number'
66
+ ? wrap(node)
67
+ : node;
68
+ }
69
+
70
+ /**
71
+ * Centered placeholder for a screen, list, or panel with nothing to show:
72
+ * a large icon, a title, optional supporting copy, and an optional action.
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * <EmptyState
77
+ * icon="status.info"
78
+ * title="No results"
79
+ * description="Try a different search term or clear your filters."
80
+ * action={<Button onPress={reset}>Clear filters</Button>}
81
+ * />
82
+ * ```
83
+ */
84
+ export const EmptyState = forwardRef<
85
+ ComponentRef<typeof View>,
86
+ EmptyStateProps
87
+ >(function EmptyState(
88
+ {
89
+ icon,
90
+ iconColor = 'text.default',
91
+ media,
92
+ title,
93
+ description,
94
+ action,
95
+ size = 'md',
96
+ fill = false,
97
+ style,
98
+ ...rest
99
+ },
100
+ ref
101
+ ) {
102
+ styles.useVariants({ size, fill });
103
+
104
+ const typography = TYPOGRAPHY[size];
105
+
106
+ return (
107
+ <View ref={ref} style={[styles.root, style]} {...rest}>
108
+ {media ??
109
+ (icon ? (
110
+ <View style={styles.iconBadge}>
111
+ <Icon name={icon} size={ICON_SIZE[size]} color={iconColor} />
112
+ </View>
113
+ ) : null)}
114
+
115
+ <View style={styles.text}>
116
+ {renderSlot(title, (value) => (
117
+ <Text weight="semibold" variant={typography.title} align="center">
118
+ {value}
119
+ </Text>
120
+ ))}
121
+ {renderSlot(description, (value) => (
122
+ <Text
123
+ variant={typography.description}
124
+ color="text.subtle"
125
+ align="center"
126
+ >
127
+ {value}
128
+ </Text>
129
+ ))}
130
+ </View>
131
+
132
+ {action ? <View style={styles.actions}>{action}</View> : null}
133
+ </View>
134
+ );
135
+ });
136
+
137
+ // ─── Styles ───────────────────────────────────────────────────────────────────
138
+ //
139
+ // `size` and `fill` are the only style axes, and neither interacts with the
140
+ // other, so a plain variant per element does the whole job — no compound
141
+ // variants needed.
142
+ // The badge is sized by padding around the icon rather than an explicit
143
+ // width/height, which keeps it a perfect circle on the spacing scale.
144
+ const styles = StyleSheet.create((theme) => ({
145
+ root: {
146
+ alignItems: 'center',
147
+ justifyContent: 'center',
148
+ variants: {
149
+ // `alignSelf` matters as much as `flex` here: a column parent hands its
150
+ // children the cross-axis size of their content unless they stretch, so
151
+ // without it the state would fill the height and still be pinned to the
152
+ // width of its longest line.
153
+ fill: {
154
+ true: { flex: 1, alignSelf: 'stretch' },
155
+ false: {},
156
+ },
157
+ size: {
158
+ sm: {
159
+ gap: theme.spacing['2'],
160
+ paddingVertical: theme.spacing['4'],
161
+ paddingHorizontal: theme.spacing['4'],
162
+ },
163
+ md: {
164
+ gap: theme.spacing['3'],
165
+ paddingVertical: theme.spacing['6'],
166
+ paddingHorizontal: theme.spacing['6'],
167
+ },
168
+ lg: {
169
+ gap: theme.spacing['4'],
170
+ paddingVertical: theme.spacing['8'],
171
+ paddingHorizontal: theme.spacing['6'],
172
+ },
173
+ },
174
+ },
175
+ },
176
+ iconBadge: {
177
+ alignItems: 'center',
178
+ justifyContent: 'center',
179
+ backgroundColor: theme.color['action.bg.primary.subtle'],
180
+ borderRadius: theme.radius.full,
181
+ variants: {
182
+ size: {
183
+ sm: { padding: theme.spacing['3'] },
184
+ md: { padding: theme.spacing['4'] },
185
+ lg: { padding: theme.spacing['5'] },
186
+ },
187
+ },
188
+ },
189
+ text: {
190
+ alignItems: 'center',
191
+ variants: {
192
+ size: {
193
+ sm: { gap: theme.spacing['1'] },
194
+ md: { gap: theme.spacing['1'] },
195
+ lg: { gap: theme.spacing['2'] },
196
+ },
197
+ },
198
+ },
199
+ actions: {
200
+ alignItems: 'center',
201
+ variants: {
202
+ size: {
203
+ sm: { gap: theme.spacing['2'], marginTop: theme.spacing['1'] },
204
+ md: { gap: theme.spacing['3'], marginTop: theme.spacing['2'] },
205
+ lg: { gap: theme.spacing['3'], marginTop: theme.spacing['2'] },
206
+ },
207
+ },
208
+ },
209
+ }));