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.
- package/CHANGELOG.md +39 -0
- package/lib/module/components/chip/Chip.js +97 -41
- package/lib/module/components/chip/Chip.js.map +1 -1
- package/lib/module/components/chip/utils.js +3 -1
- package/lib/module/components/chip/utils.js.map +1 -1
- package/lib/module/components/header/variants/LargeTitleHeader/index.js +50 -16
- package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
- package/lib/module/components/screen/ScreenFlashList.js +32 -26
- package/lib/module/components/screen/ScreenFlashList.js.map +1 -1
- package/lib/module/components/screen/ScreenFlatList.js +37 -31
- package/lib/module/components/screen/ScreenFlatList.js.map +1 -1
- package/lib/module/components/screen/ScreenScrollView.js +24 -18
- package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
- package/lib/module/components/text/Text.js +2 -0
- package/lib/module/components/text/Text.js.map +1 -1
- package/lib/module/context/ThemeProvider.js +15 -0
- package/lib/module/context/ThemeProvider.js.map +1 -1
- package/lib/module/context/index.js +1 -1
- package/lib/module/context/index.js.map +1 -1
- package/lib/module/theme/breakpoints.js +29 -0
- package/lib/module/theme/breakpoints.js.map +1 -0
- package/lib/module/theme/index.js +4 -0
- package/lib/module/theme/index.js.map +1 -1
- package/lib/module/theme/registry.js +3 -1
- package/lib/module/theme/registry.js.map +1 -1
- package/lib/module/theme/screen-margin.js +17 -0
- package/lib/module/theme/screen-margin.js.map +1 -0
- package/lib/module/theme/tokens/dark.js +3 -3
- package/lib/module/theme/tokens/dark.js.map +1 -1
- package/lib/module/theme/tokens/primitives.js +12 -2
- package/lib/module/theme/tokens/primitives.js.map +1 -1
- package/lib/typescript/src/components/chip/Chip.d.ts.map +1 -1
- package/lib/typescript/src/components/chip/utils.d.ts.map +1 -1
- package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts +9 -3
- package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
- package/lib/typescript/src/components/screen/ScreenFlashList.d.ts.map +1 -1
- package/lib/typescript/src/components/screen/ScreenFlatList.d.ts.map +1 -1
- package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
- package/lib/typescript/src/components/text/Text.d.ts +1 -0
- package/lib/typescript/src/components/text/Text.d.ts.map +1 -1
- package/lib/typescript/src/context/ThemeProvider.d.ts +8 -0
- package/lib/typescript/src/context/ThemeProvider.d.ts.map +1 -1
- package/lib/typescript/src/context/index.d.ts +1 -1
- package/lib/typescript/src/context/index.d.ts.map +1 -1
- package/lib/typescript/src/theme/breakpoints.d.ts +32 -0
- package/lib/typescript/src/theme/breakpoints.d.ts.map +1 -0
- package/lib/typescript/src/theme/index.d.ts +4 -0
- package/lib/typescript/src/theme/index.d.ts.map +1 -1
- package/lib/typescript/src/theme/registry.d.ts.map +1 -1
- package/lib/typescript/src/theme/screen-margin.d.ts +15 -0
- package/lib/typescript/src/theme/screen-margin.d.ts.map +1 -0
- package/lib/typescript/src/theme/tokens/primitives.d.ts +10 -2
- package/lib/typescript/src/theme/tokens/primitives.d.ts.map +1 -1
- package/lib/typescript/src/theme/types.d.ts +9 -3
- package/lib/typescript/src/theme/types.d.ts.map +1 -1
- package/package.json +1 -2
- package/src/components/chip/Chip.tsx +94 -48
- package/src/components/chip/utils.ts +8 -1
- package/src/components/header/variants/LargeTitleHeader/index.tsx +56 -15
- package/src/components/screen/ScreenFlashList.tsx +37 -30
- package/src/components/screen/ScreenFlatList.tsx +41 -34
- package/src/components/screen/ScreenScrollView.tsx +24 -18
- package/src/components/text/Text.tsx +3 -0
- package/src/context/ThemeProvider.tsx +15 -0
- package/src/context/index.ts +1 -1
- package/src/theme/breakpoints.ts +41 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/registry.ts +2 -1
- package/src/theme/screen-margin.ts +26 -0
- package/src/theme/tokens/dark.ts +3 -3
- package/src/theme/tokens/primitives.ts +12 -2
- package/src/theme/types.ts +12 -2
- package/skills/terra-ui/SKILL.md +0 -193
- package/skills/terra-ui/references/recipes.md +0 -198
- 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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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(
|
package/src/context/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/theme/index.ts
CHANGED
|
@@ -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 ────────────────────────────────────────────────────────────────────
|
package/src/theme/registry.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/theme/tokens/dark.ts
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export const dark = {
|
|
9
9
|
// ── color: page background ────────────────────────────────────────────────
|
|
10
|
-
'color.background': '
|
|
10
|
+
'color.background': '#050505',
|
|
11
11
|
|
|
12
12
|
// ── color: shadow (used as elevation.*.shadowColor) ───────────────────────
|
|
13
|
-
'color.shadow': '#
|
|
13
|
+
'color.shadow': '#FFFFFF',
|
|
14
14
|
|
|
15
15
|
// ── color: surfaces (bg implied — no property segment) ──────────────────
|
|
16
|
-
'color.surface.default': '
|
|
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
|
-
|
|
454
|
-
|
|
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
|
package/src/theme/types.ts
CHANGED
|
@@ -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
|
-
/**
|
|
225
|
-
|
|
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. */
|
package/skills/terra-ui/SKILL.md
DELETED
|
@@ -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
|