react-native-terra-ui 0.10.1 → 0.11.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 (86) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +8 -4
  3. package/lib/module/components/button/Button.js +21 -13
  4. package/lib/module/components/button/Button.js.map +1 -1
  5. package/lib/module/components/button/index.js.map +1 -1
  6. package/lib/module/components/divider/Divider.js +127 -0
  7. package/lib/module/components/divider/Divider.js.map +1 -0
  8. package/lib/module/components/divider/index.js +4 -0
  9. package/lib/module/components/divider/index.js.map +1 -0
  10. package/lib/module/components/index.js +2 -0
  11. package/lib/module/components/index.js.map +1 -1
  12. package/lib/module/components/pressable/index.js +4 -0
  13. package/lib/module/components/pressable/index.js.map +1 -0
  14. package/lib/module/components/pressable/variants/PressableScale/index.js +110 -0
  15. package/lib/module/components/pressable/variants/PressableScale/index.js.map +1 -0
  16. package/lib/module/context/ThemeProvider.js +5 -5
  17. package/lib/module/context/ThemeProvider.js.map +1 -1
  18. package/lib/module/theme/define-config.js +5 -5
  19. package/lib/module/theme/define-config.js.map +1 -1
  20. package/lib/module/theme/errors.js +1 -1
  21. package/lib/module/theme/index.js +2 -2
  22. package/lib/module/theme/index.js.map +1 -1
  23. package/lib/module/theme/registry.js +15 -21
  24. package/lib/module/theme/registry.js.map +1 -1
  25. package/lib/module/theme/resolve-config.js +68 -68
  26. package/lib/module/theme/resolve-config.js.map +1 -1
  27. package/lib/module/theme/runtime.js +11 -11
  28. package/lib/module/theme/runtime.js.map +1 -1
  29. package/lib/module/theme/selection-store.js +7 -7
  30. package/lib/module/theme/selection-store.js.map +1 -1
  31. package/lib/module/theme/unistyles-adapter.js +1 -1
  32. package/lib/module/utils/accent-utils.js +24 -24
  33. package/lib/module/utils/accent-utils.js.map +1 -1
  34. package/lib/typescript/src/components/button/Button.d.ts +7 -0
  35. package/lib/typescript/src/components/button/Button.d.ts.map +1 -1
  36. package/lib/typescript/src/components/button/index.d.ts +1 -1
  37. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  38. package/lib/typescript/src/components/divider/Divider.d.ts +49 -0
  39. package/lib/typescript/src/components/divider/Divider.d.ts.map +1 -0
  40. package/lib/typescript/src/components/divider/index.d.ts +3 -0
  41. package/lib/typescript/src/components/divider/index.d.ts.map +1 -0
  42. package/lib/typescript/src/components/index.d.ts +2 -0
  43. package/lib/typescript/src/components/index.d.ts.map +1 -1
  44. package/lib/typescript/src/components/pressable/index.d.ts +3 -0
  45. package/lib/typescript/src/components/pressable/index.d.ts.map +1 -0
  46. package/lib/typescript/src/components/pressable/variants/PressableScale/index.d.ts +47 -0
  47. package/lib/typescript/src/components/pressable/variants/PressableScale/index.d.ts.map +1 -0
  48. package/lib/typescript/src/context/ThemeProvider.d.ts +7 -7
  49. package/lib/typescript/src/context/ThemeProvider.d.ts.map +1 -1
  50. package/lib/typescript/src/theme/define-config.d.ts +11 -8
  51. package/lib/typescript/src/theme/define-config.d.ts.map +1 -1
  52. package/lib/typescript/src/theme/errors.d.ts +1 -1
  53. package/lib/typescript/src/theme/index.d.ts +3 -3
  54. package/lib/typescript/src/theme/index.d.ts.map +1 -1
  55. package/lib/typescript/src/theme/registry.d.ts +5 -5
  56. package/lib/typescript/src/theme/registry.d.ts.map +1 -1
  57. package/lib/typescript/src/theme/resolve-config.d.ts +25 -25
  58. package/lib/typescript/src/theme/resolve-config.d.ts.map +1 -1
  59. package/lib/typescript/src/theme/runtime.d.ts +5 -5
  60. package/lib/typescript/src/theme/runtime.d.ts.map +1 -1
  61. package/lib/typescript/src/theme/selection-store.d.ts +5 -5
  62. package/lib/typescript/src/theme/selection-store.d.ts.map +1 -1
  63. package/lib/typescript/src/theme/types.d.ts +20 -47
  64. package/lib/typescript/src/theme/types.d.ts.map +1 -1
  65. package/lib/typescript/src/theme/unistyles-adapter.d.ts +1 -1
  66. package/lib/typescript/src/utils/accent-utils.d.ts +6 -7
  67. package/lib/typescript/src/utils/accent-utils.d.ts.map +1 -1
  68. package/package.json +1 -1
  69. package/src/components/button/Button.tsx +32 -13
  70. package/src/components/button/index.ts +1 -0
  71. package/src/components/divider/Divider.tsx +164 -0
  72. package/src/components/divider/index.ts +7 -0
  73. package/src/components/index.ts +2 -0
  74. package/src/components/pressable/index.ts +5 -0
  75. package/src/components/pressable/variants/PressableScale/index.tsx +166 -0
  76. package/src/context/ThemeProvider.tsx +10 -10
  77. package/src/theme/define-config.ts +12 -7
  78. package/src/theme/errors.ts +1 -1
  79. package/src/theme/index.ts +6 -8
  80. package/src/theme/registry.ts +15 -23
  81. package/src/theme/resolve-config.ts +88 -95
  82. package/src/theme/runtime.ts +11 -11
  83. package/src/theme/selection-store.ts +9 -9
  84. package/src/theme/types.ts +21 -47
  85. package/src/theme/unistyles-adapter.ts +1 -1
  86. package/src/utils/accent-utils.ts +25 -25
@@ -0,0 +1,166 @@
1
+ import { type ComponentRef, forwardRef, useCallback, useEffect, useState } from "react";
2
+ import type {
3
+ GestureResponderEvent,
4
+ PressableProps,
5
+ PressableStateCallbackType,
6
+ StyleProp,
7
+ ViewStyle,
8
+ } from "react-native";
9
+ import { Pressable } from "react-native";
10
+
11
+ import Animated, {
12
+ Easing,
13
+ interpolate,
14
+ useAnimatedStyle,
15
+ useSharedValue,
16
+ type WithSpringConfig,
17
+ type WithTimingConfig,
18
+ withSpring,
19
+ withTiming,
20
+ } from "react-native-reanimated";
21
+ import { StyleSheet } from "react-native-unistyles";
22
+
23
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
24
+
25
+ /** A 250ms ease that reads as instant on press-in but never snaps back on release. */
26
+ const DEFAULT_TIMING: WithTimingConfig = {
27
+ duration: 250,
28
+ easing: Easing.bezier(0.25, 0.1, 0.25, 1),
29
+ };
30
+
31
+ export type PressableScaleAnimationConfig =
32
+ | { type: "timing"; config?: WithTimingConfig }
33
+ | { type: "spring"; config?: WithSpringConfig };
34
+
35
+ export interface PressableScaleProps extends Omit<PressableProps, "style" | "disabled"> {
36
+ /** Scale held at rest. Defaults to `1`. */
37
+ baseScale?: number;
38
+ /**
39
+ * Scale held while pressed. Values below `baseScale` shrink, above it grow.
40
+ * Defaults to `0.96`.
41
+ */
42
+ minScale?: number;
43
+ /**
44
+ * Opacity held while pressed, animated on the same curve as the scale.
45
+ * Defaults to `undefined` — opacity is left untouched so `style` keeps
46
+ * ownership of it.
47
+ */
48
+ pressedOpacity?: number;
49
+ /**
50
+ * Drives the press animation. Defaults to `{ type: 'timing' }` with a 250ms
51
+ * ease; pass `{ type: 'spring' }` for a springy release.
52
+ */
53
+ animationConfig?: PressableScaleAnimationConfig;
54
+ /** Blocks presses and holds the resting scale. Defaults to `false`. */
55
+ isDisabled?: boolean;
56
+ /**
57
+ * Styles for the pressable itself. The function form receives `pressed`
58
+ * only — `hovered` and `focused` are not tracked, because the animated
59
+ * style has to be merged outside of `Pressable`'s own callback.
60
+ */
61
+ style?: StyleProp<ViewStyle> | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>);
62
+ }
63
+
64
+ /**
65
+ * A `Pressable` that animates a scale (and optionally an opacity) between its
66
+ * resting and pressed states, driven by a single shared `progress` value on the
67
+ * UI thread.
68
+ *
69
+ * Use it anywhere a press deserves physical feedback — cards, list rows, custom
70
+ * controls. `Button` renders one internally via its `animation` prop.
71
+ */
72
+ export const PressableScale = forwardRef<ComponentRef<typeof Pressable>, PressableScaleProps>(
73
+ function PressableScale(
74
+ {
75
+ baseScale = 1,
76
+ minScale = 0.96,
77
+ pressedOpacity,
78
+ animationConfig,
79
+ isDisabled = false,
80
+ style,
81
+ onPressIn,
82
+ onPressOut,
83
+ ...rest
84
+ },
85
+ ref
86
+ ) {
87
+ const progress = useSharedValue(0);
88
+
89
+ // Only the function form of `style` needs a re-render to resolve; tracking
90
+ // `pressed` unconditionally would re-render every consumer on every touch.
91
+ const isStyleFunction = typeof style === "function";
92
+ const [pressed, setPressed] = useState(false);
93
+
94
+ const animateTo = useCallback(
95
+ (to: number) => {
96
+ if (animationConfig?.type === "spring") {
97
+ progress.value = withSpring(to, animationConfig.config);
98
+ return;
99
+ }
100
+ progress.value = withTiming(to, { ...DEFAULT_TIMING, ...animationConfig?.config });
101
+ },
102
+ [animationConfig, progress]
103
+ );
104
+
105
+ const handlePressIn = useCallback(
106
+ (event: GestureResponderEvent) => {
107
+ animateTo(1);
108
+ if (isStyleFunction) setPressed(true);
109
+ onPressIn?.(event);
110
+ },
111
+ [animateTo, isStyleFunction, onPressIn]
112
+ );
113
+
114
+ const handlePressOut = useCallback(
115
+ (event: GestureResponderEvent) => {
116
+ animateTo(0);
117
+ if (isStyleFunction) setPressed(false);
118
+ onPressOut?.(event);
119
+ },
120
+ [animateTo, isStyleFunction, onPressOut]
121
+ );
122
+
123
+ // A press that disables its own pressable — `onPress` flipping a button into
124
+ // a loading state — never gets an `onPressOut`, so release it here instead
125
+ // of leaving it stuck at the pressed scale.
126
+ useEffect(() => {
127
+ if (!isDisabled) return;
128
+ animateTo(0);
129
+ setPressed(false);
130
+ }, [isDisabled, animateTo]);
131
+
132
+ const animatedStyle = useAnimatedStyle(() => {
133
+ const scale = interpolate(progress.value, [0, 1], [baseScale, minScale]);
134
+ if (pressedOpacity === undefined) {
135
+ return { transform: [{ scale }] };
136
+ }
137
+ return {
138
+ transform: [{ scale }],
139
+ opacity: interpolate(progress.value, [0, 1], [1, pressedOpacity]),
140
+ };
141
+ });
142
+
143
+ const pressableState: PressableStateCallbackType = { pressed };
144
+
145
+ return (
146
+ <AnimatedPressable
147
+ ref={ref}
148
+ disabled={isDisabled}
149
+ {...rest}
150
+ onPressIn={handlePressIn}
151
+ onPressOut={handlePressOut}
152
+ // The animated style goes first so anything the caller sets — a disabled
153
+ // opacity, say — still wins.
154
+ style={[styles.root, animatedStyle, isStyleFunction ? style(pressableState) : style]}
155
+ />
156
+ );
157
+ }
158
+ );
159
+
160
+ // ─── Styles ───────────────────────────────────────────────────────────────────
161
+ //
162
+ // PressableScale has no style of its own — this stylesheet exists so the
163
+ // Unistyles Babel plugin swaps the `Pressable` imported above for its
164
+ // Unistyles-aware counterpart. Without it a themed style handed in through
165
+ // `style` freezes at whichever theme was active when it was created.
166
+ const styles = StyleSheet.create(() => ({ root: {} }));
@@ -5,9 +5,9 @@ import { useUnistyles } from "react-native-unistyles";
5
5
 
6
6
  import { bootstrapTerraUI } from "../theme/registry";
7
7
  import {
8
- applyAccent,
9
8
  applyScheme,
10
9
  getThemeSelection,
10
+ setTheme,
11
11
  setThemeMode,
12
12
  subscribeThemeSelection,
13
13
  } from "../theme/runtime";
@@ -41,13 +41,13 @@ export interface UseThemeResult {
41
41
  scheme: Scheme;
42
42
  /** Pin a scheme (stops following the system). Same as `setMode(scheme)`. */
43
43
  setScheme: (scheme: Scheme) => void;
44
- /** Applied accent name, or `undefined` for the base themes. */
45
- accent: string | undefined;
44
+ /** Applied theme name, or `undefined` for the base themes. */
45
+ themeName: string | undefined;
46
46
  /**
47
- * Apply a registered accent; `null` restores the base themes. An unknown
48
- * name warns in development and leaves the accent unchanged.
47
+ * Apply a registered theme; `null` restores the base themes. An unknown
48
+ * name warns in development and leaves the theme unchanged.
49
49
  */
50
- setAccent: (name: string | null) => void;
50
+ setTheme: (name: string | null) => void;
51
51
  /**
52
52
  * How the active scheme is chosen: `system` follows the OS appearance,
53
53
  * `light` / `dark` pin a scheme. A system appearance change updates
@@ -59,8 +59,8 @@ export interface UseThemeResult {
59
59
  }
60
60
 
61
61
  /**
62
- * Access the active theme and switch scheme / mode / accent. Selection state
63
- * comes from the runtime store, so changes made imperatively (`applyAccent`,
62
+ * Access the active theme and switch scheme / mode / theme. Selection state
63
+ * comes from the runtime store, so changes made imperatively (`setTheme`,
64
64
  * `setThemeMode`) and through this hook are observed by every consumer.
65
65
  */
66
66
  export function useTheme(): UseThemeResult {
@@ -71,8 +71,8 @@ export function useTheme(): UseThemeResult {
71
71
  theme,
72
72
  scheme: (rt.themeName ?? "light") as Scheme,
73
73
  setScheme: applyScheme,
74
- accent: selection.accent,
75
- setAccent: applyAccent,
74
+ themeName: selection.themeName,
75
+ setTheme,
76
76
  mode: selection.mode,
77
77
  setMode: setThemeMode,
78
78
  };
@@ -1,7 +1,7 @@
1
1
  import type { TerraConfig } from "./types";
2
2
 
3
- /** The literal accent names declared in `T.accents`, or `never` when none are declared. */
4
- type DeclaredAccentNames<T> = T extends { accents: infer A }
3
+ /** The literal theme names declared in `T.themes`, or `never` when none are declared. */
4
+ type DeclaredThemeNames<T> = T extends { themes: infer A }
5
5
  ? A extends Record<string, unknown>
6
6
  ? keyof A & string
7
7
  : never
@@ -9,19 +9,24 @@ type DeclaredAccentNames<T> = T extends { accents: infer A }
9
9
 
10
10
  /**
11
11
  * Identity helper for authoring a {@link TerraConfig} with contextual typing.
12
- * Has no side effects. With literal `accents`, `defaultAccent` must name one of
13
- * them; a dynamic `Record<string, Accent>` keeps `defaultAccent` as `string`
12
+ * Has no side effects. With literal `themes`, `defaultTheme` must name one of
13
+ * them; a dynamic `Record<string, NamedTheme>` keeps `defaultTheme` as `string`
14
14
  * and defers to runtime validation.
15
15
  *
16
16
  * @example
17
17
  * const config = defineTerraConfig({
18
- * accents: { indigo: { light: "#4f46e5", dark: "#818cf8" } },
19
- * defaultAccent: "indigo",
18
+ * themes: { indigo: { light: "#4f46e5", dark: "#818cf8" } },
19
+ * defaultTheme: "indigo",
20
20
  * });
21
21
  * configureTerraUI(config);
22
22
  */
23
23
  export function defineTerraConfig<const T extends TerraConfig>(
24
- config: T & { defaultAccent?: DeclaredAccentNames<T> }
24
+ config: T & {
25
+ defaultTheme?: DeclaredThemeNames<T>;
26
+ schemes?: never;
27
+ accents?: never;
28
+ defaultAccent?: never;
29
+ }
25
30
  ): T {
26
31
  return config;
27
32
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Thrown for invalid pure configuration input — before any registry, selection,
3
3
  * or native state is touched. `path` points at the offending config entry, e.g.
4
- * `accents.indigo.dark.seed` or `defaultAccent`.
4
+ * `themes.indigo.dark.seed` or `defaultTheme`.
5
5
  */
6
6
  export class TerraConfigError extends Error {
7
7
  readonly path: string;
@@ -33,23 +33,23 @@ export { defineTerraConfig } from "./define-config";
33
33
  export { TerraConfigError } from "./errors";
34
34
  export {
35
35
  configureTerraUI,
36
- getAccentNames,
37
36
  getDefaultRadius,
38
37
  getIcon,
39
38
  getImageComponent,
40
39
  getIsConfigured,
41
40
  getRequiredIcon,
42
41
  getSurfaceElevation,
42
+ getThemeNames,
43
43
  resolveTheme,
44
44
  } from "./registry";
45
45
  export { createTerraThemes } from "./resolve-config";
46
46
  export type { ThemeSelection } from "./runtime";
47
47
  export {
48
- applyAccent,
49
48
  applyScheme,
50
- getCurrentAccent,
49
+ getCurrentTheme,
51
50
  getThemeMode,
52
51
  getThemeSelection,
52
+ setTheme,
53
53
  setThemeMode,
54
54
  } from "./runtime";
55
55
  export type { ResolvedScreenMargin } from "./screen-margin";
@@ -60,12 +60,7 @@ export { resolveScreenPadding } from "./screen-padding";
60
60
  export { defaultDarkTheme, defaultLightTheme } from "./theme";
61
61
  // ─── Types ────────────────────────────────────────────────────────────────────
62
62
  export type {
63
- Accent,
64
63
  AccentColorToken,
65
- AccentOverride,
66
- AccentSchemeInput,
67
- AccentSeed,
68
- AccentShorthand,
69
64
  ActionColorToken,
70
65
  BackgroundColorToken,
71
66
  BorderColorToken,
@@ -81,6 +76,7 @@ export type {
81
76
  ElevationStyle,
82
77
  FontWeightToken,
83
78
  LayoutTokens,
79
+ NamedTheme,
84
80
  NeutralColorToken,
85
81
  OpacityTokens,
86
82
  RadiusComponent,
@@ -110,6 +106,8 @@ export type {
110
106
  ThemeColor,
111
107
  ThemeMode,
112
108
  ThemeRadiusOverride,
109
+ ThemeSchemeInput,
110
+ ThemeSeed,
113
111
  ThemeSpacingOverride,
114
112
  TypeStyle,
115
113
  Typography,
@@ -23,7 +23,6 @@ import type {
23
23
  TerraImageComponent,
24
24
  TerraSemanticIconName,
25
25
  TerraTheme,
26
- TerraThemeInput,
27
26
  ThemeMode,
28
27
  } from "./types";
29
28
  import { applyMode, configureUnistyles, hasConfiguredUnistyles, updateThemes } from "./unistyles-adapter";
@@ -81,8 +80,8 @@ let installation: "unconfigured" | "fallback" | "app" = "unconfigured";
81
80
 
82
81
  export const getIsConfigured = (): boolean => installation !== "unconfigured";
83
82
 
84
- /** Names of the registered accents, in declaration order. Returns a fresh array. */
85
- export const getAccentNames = (): string[] => [...getInstalledConfig().accentNames];
83
+ /** Names of the registered themes, in declaration order. Returns a fresh array. */
84
+ export const getThemeNames = (): string[] => [...getInstalledConfig().themeNames];
86
85
 
87
86
  export const getImageComponent = (): TerraImageComponent => settings.imageComponent;
88
87
 
@@ -107,22 +106,22 @@ export const getSurfaceElevation = (): ElevationKey => settings.surfaceElevation
107
106
 
108
107
  /**
109
108
  * Resolves one scheme's theme from the installed configuration with the named
110
- * accent overlaid. Omitting `accentName` selects the base theme (no accent
111
- * not the configured `defaultAccent`). An unknown name warns in development
109
+ * theme selected. Omitting `name` selects the base theme (defaults + shared
110
+ * not the configured `defaultTheme`). An unknown name warns in development
112
111
  * and falls back to the base theme. Pure with respect to runtime state: it
113
112
  * never changes the applied selection.
114
113
  */
115
- export function resolveTheme(scheme: Scheme, accentName?: string): TerraTheme {
114
+ export function resolveTheme(scheme: Scheme, name?: string): TerraTheme {
116
115
  const config = getInstalledConfig();
117
- let accent: string | null = null;
118
- if (accentName !== undefined) {
119
- if (config.accentNames.includes(accentName)) {
120
- accent = accentName;
116
+ let themeName: string | null = null;
117
+ if (name !== undefined) {
118
+ if (config.themeNames.includes(name)) {
119
+ themeName = name;
121
120
  } else if (__DEV__) {
122
- console.warn(`[react-native-terra-ui] Unknown accent "${accentName}"; resolving the base theme.`);
121
+ console.warn(`[react-native-terra-ui] Unknown theme "${name}"; resolving the base theme.`);
123
122
  }
124
123
  }
125
- return resolveThemeScheme(config, scheme, accent).theme;
124
+ return resolveThemeScheme(config, scheme, themeName).theme;
126
125
  }
127
126
 
128
127
  // ─── Configuration ───────────────────────────────────────────────────────────
@@ -189,22 +188,15 @@ function deriveMode(config: TerraConfig): ThemeMode {
189
188
  return "system";
190
189
  }
191
190
 
192
- const pickThemeInput = (config: TerraConfig): TerraThemeInput => ({
193
- shared: config.shared,
194
- schemes: config.schemes,
195
- accents: config.accents,
196
- defaultAccent: config.defaultAccent,
197
- });
198
-
199
191
  function applyConfig(config: TerraConfig, kind: "fallback" | "app"): void {
200
192
  // (a) Pure: validate and build everything. Throws leave ALL state untouched.
201
- const themeConfig = normalizeThemeInput(pickThemeInput(config));
193
+ const themeConfig = normalizeThemeInput(config);
202
194
  const mode = deriveMode(config);
203
- const { themes, diagnostics } = resolveThemes(themeConfig, themeConfig.defaultAccent);
195
+ const { themes, diagnostics } = resolveThemes(themeConfig, themeConfig.defaultTheme);
204
196
  const nextSettings = buildSettings(config);
205
197
 
206
198
  // (b) Native. `StyleSheet.configure` runs once per app; after that the
207
- // rebuilt themes reach the runtime the same way an accent change does, so an
199
+ // rebuilt themes reach the runtime the same way a theme change does, so an
208
200
  // app's config is never stranded behind the fallback's defaults.
209
201
  if (hasConfiguredUnistyles()) {
210
202
  updateThemes(themes);
@@ -217,7 +209,7 @@ function applyConfig(config: TerraConfig, kind: "fallback" | "app"): void {
217
209
  installation = kind;
218
210
  settings = nextSettings;
219
211
  setInstalledConfig(themeConfig);
220
- publishThemeSelection({ accent: themeConfig.defaultAccent ?? undefined, mode });
212
+ publishThemeSelection({ themeName: themeConfig.defaultTheme ?? undefined, mode });
221
213
 
222
214
  warnContrastDiagnostics(diagnostics);
223
215
  }