react-native-terra-ui 0.2.2 → 0.4.0

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 (75) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/lib/module/components/chip/Chip.js +97 -41
  3. package/lib/module/components/chip/Chip.js.map +1 -1
  4. package/lib/module/components/chip/utils.js +3 -1
  5. package/lib/module/components/chip/utils.js.map +1 -1
  6. package/lib/module/components/header/variants/LargeTitleHeader/index.js +50 -16
  7. package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
  8. package/lib/module/components/screen/ScreenFlashList.js +32 -26
  9. package/lib/module/components/screen/ScreenFlashList.js.map +1 -1
  10. package/lib/module/components/screen/ScreenFlatList.js +37 -31
  11. package/lib/module/components/screen/ScreenFlatList.js.map +1 -1
  12. package/lib/module/components/screen/ScreenScrollView.js +24 -18
  13. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  14. package/lib/module/components/text/Text.js +2 -0
  15. package/lib/module/components/text/Text.js.map +1 -1
  16. package/lib/module/context/ThemeProvider.js +15 -0
  17. package/lib/module/context/ThemeProvider.js.map +1 -1
  18. package/lib/module/context/index.js +1 -1
  19. package/lib/module/context/index.js.map +1 -1
  20. package/lib/module/theme/breakpoints.js +29 -0
  21. package/lib/module/theme/breakpoints.js.map +1 -0
  22. package/lib/module/theme/index.js +4 -0
  23. package/lib/module/theme/index.js.map +1 -1
  24. package/lib/module/theme/registry.js +3 -1
  25. package/lib/module/theme/registry.js.map +1 -1
  26. package/lib/module/theme/screen-margin.js +17 -0
  27. package/lib/module/theme/screen-margin.js.map +1 -0
  28. package/lib/module/theme/tokens/dark.js +3 -3
  29. package/lib/module/theme/tokens/dark.js.map +1 -1
  30. package/lib/module/theme/tokens/primitives.js +12 -2
  31. package/lib/module/theme/tokens/primitives.js.map +1 -1
  32. package/lib/typescript/src/components/chip/Chip.d.ts.map +1 -1
  33. package/lib/typescript/src/components/chip/utils.d.ts.map +1 -1
  34. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts +9 -3
  35. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
  36. package/lib/typescript/src/components/screen/ScreenFlashList.d.ts.map +1 -1
  37. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts.map +1 -1
  38. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
  39. package/lib/typescript/src/components/text/Text.d.ts +1 -0
  40. package/lib/typescript/src/components/text/Text.d.ts.map +1 -1
  41. package/lib/typescript/src/context/ThemeProvider.d.ts +8 -0
  42. package/lib/typescript/src/context/ThemeProvider.d.ts.map +1 -1
  43. package/lib/typescript/src/context/index.d.ts +1 -1
  44. package/lib/typescript/src/context/index.d.ts.map +1 -1
  45. package/lib/typescript/src/theme/breakpoints.d.ts +32 -0
  46. package/lib/typescript/src/theme/breakpoints.d.ts.map +1 -0
  47. package/lib/typescript/src/theme/index.d.ts +4 -0
  48. package/lib/typescript/src/theme/index.d.ts.map +1 -1
  49. package/lib/typescript/src/theme/registry.d.ts.map +1 -1
  50. package/lib/typescript/src/theme/screen-margin.d.ts +15 -0
  51. package/lib/typescript/src/theme/screen-margin.d.ts.map +1 -0
  52. package/lib/typescript/src/theme/tokens/primitives.d.ts +10 -2
  53. package/lib/typescript/src/theme/tokens/primitives.d.ts.map +1 -1
  54. package/lib/typescript/src/theme/types.d.ts +9 -3
  55. package/lib/typescript/src/theme/types.d.ts.map +1 -1
  56. package/package.json +1 -2
  57. package/src/components/chip/Chip.tsx +94 -48
  58. package/src/components/chip/utils.ts +8 -1
  59. package/src/components/header/variants/LargeTitleHeader/index.tsx +56 -15
  60. package/src/components/screen/ScreenFlashList.tsx +37 -30
  61. package/src/components/screen/ScreenFlatList.tsx +41 -34
  62. package/src/components/screen/ScreenScrollView.tsx +24 -18
  63. package/src/components/text/Text.tsx +3 -0
  64. package/src/context/ThemeProvider.tsx +15 -0
  65. package/src/context/index.ts +1 -1
  66. package/src/theme/breakpoints.ts +41 -0
  67. package/src/theme/index.ts +5 -0
  68. package/src/theme/registry.ts +2 -1
  69. package/src/theme/screen-margin.ts +26 -0
  70. package/src/theme/tokens/dark.ts +3 -3
  71. package/src/theme/tokens/primitives.ts +12 -2
  72. package/src/theme/types.ts +12 -2
  73. package/skills/terra-ui/SKILL.md +0 -193
  74. package/skills/terra-ui/references/recipes.md +0 -198
  75. package/skills/terra-ui/references/troubleshooting.md +0 -112
@@ -13,6 +13,8 @@ import Animated, {
13
13
  } from 'react-native-reanimated';
14
14
  import { StyleSheet } from 'react-native-unistyles';
15
15
  import { scheduleOnRN } from 'react-native-worklets';
16
+
17
+ import { resolveScreenMargin } from '#theme/screen-margin';
16
18
  import { PortalHost } from '../portal';
17
19
  import { BottomSafeArea } from './parts/BottomSafeArea';
18
20
  import { renderScreenPlaceholder } from './parts/ScreenPlaceholder';
@@ -170,22 +172,26 @@ export function ScreenScrollView({
170
172
  );
171
173
  }
172
174
 
173
- const styles = StyleSheet.create((theme, rt) => ({
174
- scrollContent: (hasHeader: boolean, margins: ScreenMargins) => ({
175
- flexGrow: 1,
176
- // The header inset and the safe area are structural — they apply whether
177
- // or not margins are on; the y margin stacks on top of them.
178
- paddingTop:
179
- (hasHeader ? theme.layout.header.height + rt.insets.top : 0) +
180
- (hasYMargin(margins) ? theme.layout.screen.margin.y : 0),
181
- // The safe area is `BottomSafeArea`'s job; counting it here as well would
182
- // leave a gap the height of the gesture bar.
183
- paddingBottom: hasYMargin(margins) ? theme.layout.screen.margin.y : 0,
184
- }),
175
+ const styles = StyleSheet.create((theme, rt) => {
176
+ const { x: marginX, y: marginY } = resolveScreenMargin(theme, rt.breakpoint);
177
+
178
+ return {
179
+ scrollContent: (hasHeader: boolean, margins: ScreenMargins) => ({
180
+ flexGrow: 1,
181
+ // The header inset and the safe area are structural — they apply
182
+ // whether or not margins are on; the y margin stacks on top of them.
183
+ paddingTop:
184
+ (hasHeader ? theme.layout.header.height + rt.insets.top : 0) +
185
+ (hasYMargin(margins) ? marginY : 0),
186
+ // The safe area is `BottomSafeArea`'s job; counting it here as well
187
+ // would leave a gap the height of the gesture bar.
188
+ paddingBottom: hasYMargin(margins) ? marginY : 0,
189
+ }),
185
190
 
186
- content: (horizontal: boolean, margins: ScreenMargins) => ({
187
- flexGrow: 1,
188
- paddingHorizontal: hasXMargin(margins) ? theme.layout.screen.margin.x : 0,
189
- flexDirection: horizontal ? 'row' : 'column',
190
- }),
191
- }));
191
+ content: (horizontal: boolean, margins: ScreenMargins) => ({
192
+ flexGrow: 1,
193
+ paddingHorizontal: hasXMargin(margins) ? marginX : 0,
194
+ flexDirection: horizontal ? 'row' : 'column',
195
+ }),
196
+ };
197
+ });
@@ -44,6 +44,7 @@ export interface TextProps extends RNTextProps {
44
44
  align?: TextAlign;
45
45
  italic?: boolean;
46
46
  underline?: boolean;
47
+ transform?: 'uppercase' | 'lowercase' | 'capitalize';
47
48
  strikeThrough?: boolean;
48
49
  /** Truncate to a single line with a trailing ellipsis. */
49
50
  isTruncated?: boolean;
@@ -62,6 +63,7 @@ export const Text = forwardRef<ComponentRef<typeof RNText>, TextProps>(
62
63
  align,
63
64
  italic,
64
65
  underline,
66
+ transform,
65
67
  strikeThrough,
66
68
  isTruncated,
67
69
  style,
@@ -107,6 +109,7 @@ export const Text = forwardRef<ComponentRef<typeof RNText>, TextProps>(
107
109
  base.textDecorationLine = 'underline line-through';
108
110
  else if (underline) base.textDecorationLine = 'underline';
109
111
  else if (strikeThrough) base.textDecorationLine = 'line-through';
112
+ if (transform) base.textTransform = transform;
110
113
 
111
114
  return (
112
115
  <RNText
@@ -10,6 +10,10 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
10
10
  import { useUnistyles } from 'react-native-unistyles';
11
11
  import { bootstrapTerraUI } from '../theme/registry';
12
12
  import { applyAccent, applyScheme, getCurrentAccent } from '../theme/runtime';
13
+ import {
14
+ type ResolvedScreenMargin,
15
+ resolveScreenMargin,
16
+ } from '../theme/screen-margin';
13
17
  import type { Scheme, TerraTheme } from '../theme/types';
14
18
 
15
19
  // Ensure the default theme is registered on first import. An app that calls
@@ -88,6 +92,17 @@ export function useTheme(): UseThemeResult {
88
92
  };
89
93
  }
90
94
 
95
+ /**
96
+ * Resolves `theme.layout.screen.margin` for the current runtime breakpoint.
97
+ * For use outside a `StyleSheet.create` factory (e.g. plain-JS layout maths);
98
+ * inside one, call `resolveScreenMargin(theme, rt.breakpoint)` directly so
99
+ * the value stays reactive without a React re-render.
100
+ */
101
+ export function useScreenMargin(): ResolvedScreenMargin {
102
+ const { theme, rt } = useUnistyles();
103
+ return resolveScreenMargin(theme, rt.breakpoint);
104
+ }
105
+
91
106
  const noopSetAccent = (_name: string): void => {
92
107
  if (__DEV__) {
93
108
  console.warn(
@@ -1,2 +1,2 @@
1
1
  export type { UseThemeResult } from './ThemeProvider';
2
- export { TerraUIProvider, useTheme } from './ThemeProvider';
2
+ export { TerraUIProvider, useScreenMargin, useTheme } from './ThemeProvider';
@@ -0,0 +1,41 @@
1
+ import type { UnistylesBreakpoints } from 'react-native-unistyles';
2
+
3
+ /**
4
+ * Viewport-width breakpoints (dp), registered with Unistyles via
5
+ * `StyleSheet.configure({ breakpoints })` in {@link ./registry.ts}. `xs` must
6
+ * stay `0` — Unistyles falls back to it below `sm`.
7
+ */
8
+ export const breakpoints = {
9
+ xs: 0,
10
+ sm: 576,
11
+ md: 768,
12
+ lg: 992,
13
+ xl: 1200,
14
+ } as const;
15
+
16
+ export type AppBreakpoints = typeof breakpoints;
17
+ export type BreakpointName = keyof AppBreakpoints;
18
+
19
+ declare module 'react-native-unistyles' {
20
+ export interface UnistylesBreakpoints extends AppBreakpoints {}
21
+ }
22
+
23
+ /**
24
+ * Resolves a per-breakpoint record (e.g. `theme.layout.screen.margin.x`) to
25
+ * the value for the current runtime breakpoint (`rt.breakpoint`). A
26
+ * `StyleSheet.create` factory re-runs on breakpoint change, so calling this
27
+ * inside one keeps the resolved value reactive.
28
+ *
29
+ * `rt.breakpoint`'s type also includes Unistyles' built-in `landscape` /
30
+ * `portrait` orientation keys, which this record has no entry for — those
31
+ * (and `undefined`, below the smallest configured breakpoint) fall back to
32
+ * `xs`.
33
+ */
34
+ export function resolveBreakpointValue<T>(
35
+ values: Record<BreakpointName, T>,
36
+ breakpoint: keyof UnistylesBreakpoints | undefined
37
+ ): T {
38
+ return breakpoint !== undefined && breakpoint in values
39
+ ? values[breakpoint as BreakpointName]
40
+ : values.xs;
41
+ }
@@ -12,6 +12,9 @@ export type { DeepPartial } from '#utils/deep-merge';
12
12
  export { deepMerge } from '#utils/deep-merge';
13
13
  export { resolveThemeColor } from '#utils/resolve-theme-color';
14
14
  export { FONT_WEIGHT_VALUE, SYSTEM_FONT } from '#utils/typography';
15
+ // ─── Breakpoints ─────────────────────────────────────────────────────────────
16
+ export type { AppBreakpoints, BreakpointName } from './breakpoints';
17
+ export { breakpoints, resolveBreakpointValue } from './breakpoints';
15
18
  // ─── Config + runtime ───────────────────────────────────────────────────────
16
19
  export {
17
20
  configureTerraUI,
@@ -25,6 +28,8 @@ export {
25
28
  resolveTheme,
26
29
  } from './registry';
27
30
  export { applyAccent, applyScheme, getCurrentAccent } from './runtime';
31
+ export type { ResolvedScreenMargin } from './screen-margin';
32
+ export { resolveScreenMargin } from './screen-margin';
28
33
  // ─── Default themes ───────────────────────────────────────────────────────────
29
34
  export { defaultDarkTheme, defaultLightTheme } from './theme';
30
35
  // ─── Types ────────────────────────────────────────────────────────────────────
@@ -3,6 +3,7 @@ import { StyleSheet, UnistylesRuntime } from 'react-native-unistyles';
3
3
  import { normalizeAccent } from '#utils/accent-utils';
4
4
  import { deepMerge } from '#utils/deep-merge';
5
5
 
6
+ import { breakpoints } from './breakpoints';
6
7
  import { defaultIcons } from './defaults/icons';
7
8
  import { defaultImage } from './defaults/image';
8
9
  import {
@@ -254,6 +255,6 @@ function applyConfig(config: TerraConfig): void {
254
255
  return;
255
256
  }
256
257
 
257
- StyleSheet.configure({ themes, settings });
258
+ StyleSheet.configure({ themes, settings, breakpoints });
258
259
  unistylesConfigured = true;
259
260
  }
@@ -0,0 +1,26 @@
1
+ import type { UnistylesBreakpoints } from 'react-native-unistyles';
2
+
3
+ import { resolveBreakpointValue } from './breakpoints';
4
+ import type { TerraTheme } from './types';
5
+
6
+ export interface ResolvedScreenMargin {
7
+ x: number;
8
+ y: number;
9
+ }
10
+
11
+ /**
12
+ * Resolves both axes of `theme.layout.screen.margin` for the current runtime
13
+ * breakpoint in one call — nearly every consumer needs both axes, so this
14
+ * saves calling `resolveBreakpointValue` twice. Call inside a
15
+ * `StyleSheet.create` factory (passing `rt.breakpoint`) so the result stays
16
+ * reactive to breakpoint changes; outside one, use {@link useScreenMargin}.
17
+ */
18
+ export function resolveScreenMargin(
19
+ theme: TerraTheme,
20
+ breakpoint: keyof UnistylesBreakpoints | undefined
21
+ ): ResolvedScreenMargin {
22
+ return {
23
+ x: resolveBreakpointValue(theme.layout.screen.margin.x, breakpoint),
24
+ y: resolveBreakpointValue(theme.layout.screen.margin.y, breakpoint),
25
+ };
26
+ }
@@ -7,13 +7,13 @@
7
7
  */
8
8
  export const dark = {
9
9
  // ── color: page background ────────────────────────────────────────────────
10
- 'color.background': '{palette.neutral.950}',
10
+ 'color.background': '#050505',
11
11
 
12
12
  // ── color: shadow (used as elevation.*.shadowColor) ───────────────────────
13
- 'color.shadow': '#000000',
13
+ 'color.shadow': '#FFFFFF',
14
14
 
15
15
  // ── color: surfaces (bg implied — no property segment) ──────────────────
16
- 'color.surface.default': '{palette.neutral.900}',
16
+ 'color.surface.default': '#17191A',
17
17
  'color.surface.raised': '{palette.neutral.800}',
18
18
  'color.surface.sunken': '#050506',
19
19
  'color.surface.overlay': 'rgba(0, 0, 0, 0.72)',
@@ -450,8 +450,18 @@ export const primitives = {
450
450
  'opacity.pressed': 0.85,
451
451
 
452
452
  // ── screen-level layout ───────────────────────────────────────────────────
453
- 'layout.screen.margin.x': 16,
454
- 'layout.screen.margin.y': 0,
453
+ // `margin.x`/`margin.y` both scale up with the viewport breakpoint (see
454
+ // `theme/breakpoints.ts`).
455
+ 'layout.screen.margin.x.xs': 16,
456
+ 'layout.screen.margin.x.sm': 16,
457
+ 'layout.screen.margin.x.md': 24,
458
+ 'layout.screen.margin.x.lg': 32,
459
+ 'layout.screen.margin.x.xl': 48,
460
+ 'layout.screen.margin.y.xs': 16,
461
+ 'layout.screen.margin.y.sm': 16,
462
+ 'layout.screen.margin.y.md': 24,
463
+ 'layout.screen.margin.y.lg': 32,
464
+ 'layout.screen.margin.y.xl': 48,
455
465
 
456
466
  // ── header layout ─────────────────────────────────────────────────────────
457
467
  // Header bar height (compact nav bar / collapsed large-title bar). 56dp aligns
@@ -9,6 +9,8 @@ import type { ImageStyle, StyleProp } from 'react-native';
9
9
 
10
10
  import type { DeepPartial } from '#utils/deep-merge';
11
11
 
12
+ import type { BreakpointName } from './breakpoints';
13
+
12
14
  // ─── Scale keys (declared explicitly; token data is untyped) ────────────────
13
15
 
14
16
  /** Spacing scale keys (dp, 4dp base unit). Read as `gap={3}` → string '3'. */
@@ -221,8 +223,16 @@ export interface OpacityTokens {
221
223
  /** Screen-level layout tokens. */
222
224
  export interface LayoutTokens {
223
225
  screen: {
224
- /** Margin between content and the screen edge (applied as container padding). */
225
- margin: { x: number; y: number };
226
+ /**
227
+ * Margin between content and the screen edge (applied as container
228
+ * padding). Both axes scale with the viewport breakpoint — resolve with
229
+ * `resolveBreakpointValue(theme.layout.screen.margin.x, rt.breakpoint)`
230
+ * (same for `y`).
231
+ */
232
+ margin: {
233
+ x: Record<BreakpointName, number>;
234
+ y: Record<BreakpointName, number>;
235
+ };
226
236
  };
227
237
  header: {
228
238
  /** Height of the header bar (compact nav bar / collapsed large-title bar), dp. */
@@ -1,193 +0,0 @@
1
- ---
2
- name: terra-ui
3
- description: Build React Native UI with react-native-terra-ui — setup, component selection, theming, and the traps. Use when writing screens or components in an app that depends on react-native-terra-ui.
4
- ---
5
-
6
- # Terra UI
7
-
8
- `react-native-terra-ui` is a themed React Native component library built on Unistyles v3.
9
-
10
- **This skill ships inside the package**, so it describes the version you have installed.
11
- Check it with `node -p "require('react-native-terra-ui/package.json').version"`.
12
-
13
- **Do not guess prop names.** The package publishes both `src` and TypeScript
14
- declarations, so the real props are on disk — read
15
- `node_modules/react-native-terra-ui/lib/typescript/src/components/<name>/` or the `src`
16
- next to it. Every prop carries a JSDoc line with its default. This skill deliberately does
17
- not restate props, because a copy would drift from the types.
18
-
19
- ## Before You Write A Screen
20
-
21
- Four things break most often, in this order:
22
-
23
- 1. **Expo Go does not work.** Unistyles v3 needs native modules — use a development build
24
- (`npx expo run:ios`, or an EAS dev client). If components render unstyled or the app
25
- crashes at startup, check this first.
26
- 2. **The Babel plugin must list the library.** `react-native-unistyles/plugin` needs
27
- `autoProcessImports: ['react-native-terra-ui']` so the library's own stylesheets are
28
- processed, and `react-native-worklets/plugin` must be **last**. Without it, styles
29
- freeze at whichever theme was active when they were created.
30
- 3. **`TerraUIProvider` wraps the app once**, near the root. It supplies `SafeAreaProvider`
31
- and accent state. It does **not** include `ToastProvider` — add that separately if you
32
- use toasts.
33
- 4. **Import order decides whether your theme config wins.** See below.
34
-
35
- ## Import Order Is Load-Bearing
36
-
37
- | Entry | Side effect |
38
- |---|---|
39
- | `react-native-terra-ui/theme` | none — safe anywhere |
40
- | `react-native-terra-ui` | auto-calls `configureTerraUI()` with defaults **if not already configured** |
41
-
42
- So a custom theme must be configured *before* the first root-entry import. The pattern:
43
-
44
- ```ts
45
- // terra-ui.ts — imports from /theme only, so it has no side effects of its own
46
- import { configureTerraUI, type TerraTheme } from 'react-native-terra-ui/theme';
47
-
48
- declare module 'react-native-unistyles' {
49
- interface UnistylesThemes { light: TerraTheme; dark: TerraTheme }
50
- }
51
-
52
- configureTerraUI({ /* shared, accents, defaultAccent, icons, image, components */ });
53
- ```
54
-
55
- ```js
56
- // index.js — the bootstrap goes FIRST
57
- import './terra-ui';
58
- import 'expo-router/entry';
59
- ```
60
-
61
- Get this wrong and there is no error — the defaults silently win. If your fonts, accent, or
62
- component defaults appear to be ignored, this is why.
63
-
64
- Skip all of it if the default theme is fine; just wrap in `TerraUIProvider`.
65
-
66
- ## Styling Rules
67
-
68
- Styles must go through Unistyles, not React Native's `StyleSheet`:
69
-
70
- ```tsx
71
- import { StyleSheet } from 'react-native-unistyles'; // ✅ not from 'react-native'
72
-
73
- const styles = StyleSheet.create((theme, rt) => ({
74
- card: {
75
- backgroundColor: theme.color['surface.default'], // flat, dotted keys
76
- padding: theme.spacing['4'], // string keys, 4dp base
77
- borderRadius: theme.radius.md,
78
- paddingTop: rt.insets.top,
79
- },
80
- }));
81
- ```
82
-
83
- - **Colors are flat dotted string keys** — `theme.color['surface.default']`,
84
- `theme.color['status.danger.fg']`. The one exception is `theme.color.background`.
85
- - Semantic tokens only. There is no palette export — that is deliberate.
86
- - Put theme and variant decisions **inside** `StyleSheet.create`. Do not destructure
87
- `theme` from `useUnistyles()` to build a style object; it defeats the native styling
88
- path and stops updating on theme change.
89
- - Use `variants` / `compoundVariants` rather than conditional style arrays.
90
- - Runtime values (`insets`, screen size) come from the second `(theme, rt)` argument, never
91
- `Dimensions.get()`. No breakpoints are configured by default.
92
- - Outside a stylesheet, turn a token into a color with
93
- `resolveThemeColor(token, theme)` from `react-native-terra-ui/theme`. It passes raw
94
- literals (`#7c3aed`, `rgba(…)`, `transparent`) straight through.
95
-
96
- ## Choosing A Component
97
-
98
- | Need | Use | Not |
99
- |---|---|---|
100
- | A screen container with margins + background | `Screen` | a bare `View` |
101
- | A scrolling screen body | `Screen.ScrollView` / `.FlatList` / `.FlashList` | a plain `ScrollView` — these reserve the header inset |
102
- | A nav bar | `Screen.Header as={Header.Title}` (or `as={Header.LargeTitle}`) | a custom absolute view |
103
- | Icon actions in a header | `Toolbar.Button`, grouped in `Toolbar.Group` | `Button` with `isIconOnly` |
104
- | A labelled action | `Button` | `Toolbar.Button` |
105
- | A tag / status pill | `Chip` | `Button variant="ghost"` |
106
- | A plain themed box with token props | `Box` | `Surface`, unless you want its configured radius + elevation |
107
- | An elevated card | `Surface` | `Box` + hand-rolled shadow |
108
- | Text | `Text` with a `variant` | RN `Text` |
109
-
110
- `Box` is the only family exposing token style props (`p`, `px`, `gap`, `bg`, `radius`,
111
- `row`, `align`, `justify`, `flex`, `elevation`, …). `Surface` extends it minus `bg`.
112
- Everything else takes explicit props.
113
-
114
- `Screen.Header` is a **polymorphic marker slot**, not a wrapper. Render the header through
115
- `as`, and any extra props forward to it:
116
-
117
- ```tsx
118
- <Screen.Header as={Header.LargeTitle} title="Library" caption="24 items" />
119
- ```
120
-
121
- Nesting the header as a child still works and appears throughout older code, but `as` is
122
- the current form. Either way the slot must be a **direct** child of `Screen` — that is how
123
- `Screen` detects a header and drops its top safe-area edge.
124
-
125
- `Header.LargeTitle` collapses on scroll, so it also has to be paired with one of the
126
- `Screen` scroll containers — it reads scroll position from their shared context.
127
-
128
- ## Icons Are Registered By You
129
-
130
- The library ships only eight semantic icons: `navigation.back`, `navigation.forward`,
131
- `navigation.close`, `status.info`, `status.success`, `status.warning`, `status.danger`,
132
- `person`.
133
-
134
- Everything else is yours, registered by name through declaration merging plus the `icons`
135
- map. Both halves are required — types alone will not render, and a name that is not
136
- registered logs a dev warning and renders **nothing**:
137
-
138
- ```ts
139
- // terra-ui-icons.d.ts
140
- import type { TerraIconComponent } from 'react-native-terra-ui/theme';
141
-
142
- declare global {
143
- namespace TerraUI {
144
- interface IconRegistry { add: TerraIconComponent; share: TerraIconComponent }
145
- }
146
- }
147
- declare module 'react-native-terra-ui/theme' {
148
- interface TerraIconRegistry extends TerraUI.IconRegistry {}
149
- }
150
- declare module 'react-native-terra-ui' {
151
- interface TerraIconRegistry extends TerraUI.IconRegistry {}
152
- }
153
- ```
154
-
155
- ```ts
156
- // in configureTerraUI
157
- import { Plus, Share } from 'lucide-react-native';
158
- configureTerraUI({ icons: { add: Plus, share: Share } });
159
- ```
160
-
161
- If an icon is invisible, check that it is in **both** places.
162
-
163
- ## Theme Switching
164
-
165
- Runtime switching goes through the library, not React state:
166
-
167
- ```tsx
168
- const { theme, scheme, setScheme, accent, setAccent } = useTheme(); // under TerraUIProvider
169
- ```
170
-
171
- `setScheme` disables system-adaptive theming. `setAccent` takes a name registered in
172
- `configureTerraUI({ accents })`. Re-rendering a provider with a different theme object is
173
- not how this works — Unistyles resolves styles natively.
174
-
175
- ## Compound Components
176
-
177
- Sub-components hang off the parent: `Screen.Header` / `.ScrollView` / `.FlatList` /
178
- `.FlashList`, `Header.Title` / `.LargeTitle`, `Button.Icon` / `.Label`,
179
- `Chip.Icon`, `Toast.Title` / `.Description` / `.Action` / `.Close` / `.Icon`,
180
- `Toolbar.Group` / `.Button`, `Portal.Provider` / `.Host`.
181
-
182
- They inherit color and size from the parent through context, so
183
- `<Chip color="success"><Chip.Icon name="status.success" />Done</Chip>` needs no color on
184
- the icon. An explicit prop on the child always wins.
185
-
186
- Content goes through `children`, not `label`/`icon`/`items` props. Text children are
187
- auto-wrapped in themed `Text` where the component supports a label.
188
-
189
- ## References
190
-
191
- - `references/recipes.md` — working screen, header, toolbar, list, and toast snippets.
192
- - `references/troubleshooting.md` — symptom → cause for the failures above.
193
- - Full guide: https://github.com/earthling-std/react-native-terra-ui#readme