@mrmeg/expo-ui 0.1.3 → 0.1.5

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/LLM_USAGE.md CHANGED
@@ -16,7 +16,7 @@ import { Button as ButtonDirect } from "@mrmeg/expo-ui/components/Button";
16
16
  import { colors, spacing, typography } from "@mrmeg/expo-ui/constants";
17
17
  import { useResources, useTheme } from "@mrmeg/expo-ui/hooks";
18
18
  import { globalUIStore, useThemeStore } from "@mrmeg/expo-ui/state";
19
- import { hapticLight } from "@mrmeg/expo-ui/lib";
19
+ import { configureExpoUiI18n, hapticLight } from "@mrmeg/expo-ui/lib";
20
20
  ```
21
21
 
22
22
  The root barrel also exports the public surface:
@@ -68,6 +68,19 @@ export function RootLayout() {
68
68
  `AlertDialog`, `BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`,
69
69
  `SelectContent`, or `Tooltip`.
70
70
 
71
+ i18n is optional. Do not add app-level i18n setup just to use this package.
72
+ Plain children and `text` props work without `i18next` or `react-i18next`.
73
+ `tx` props render fallback text when provided and otherwise render the key
74
+ until the consumer opts in with a package-local translator. Package-owned
75
+ defaults such as notification titles stay human-readable without app i18n:
76
+
77
+ ```tsx
78
+ import { configureExpoUiI18n } from "@mrmeg/expo-ui/lib";
79
+ import { i18n } from "./i18n";
80
+
81
+ configureExpoUiI18n((key, options) => i18n.t(key, options));
82
+ ```
83
+
71
84
  ## Theme And Text Rules
72
85
 
73
86
  - Use `useTheme()` and semantic tokens instead of hardcoded colors.
package/README.md CHANGED
@@ -28,11 +28,11 @@ Consumers must also install the peer dependencies listed in `package.json`.
28
28
  The tested baseline is Expo SDK 55 with React 19.2, React Native 0.83,
29
29
  React Native Web 0.21, Reanimated 4.2, Worklets 0.7, and
30
30
  `@rn-primitives/*` 1.4. `@rn-primitives/portal` is package-managed because
31
- `UIProvider` mounts the portal host used by package overlays. `i18next` and
32
- `react-i18next` are runtime peers because `StyledText` and `Notification`
33
- support translated text keys. Start consumer apps from the same Expo SDK
34
- family or update the package and peer ranges deliberately. Keep npm auth tokens
35
- in developer or CI configuration, not in this repository.
31
+ `UIProvider` mounts the portal host used by package overlays. i18n setup is
32
+ optional; plain text and children render without `i18next` or
33
+ `react-i18next`. Start consumer apps from the same Expo SDK family or update
34
+ the package and peer ranges deliberately. Keep npm auth tokens in developer or
35
+ CI configuration, not in this repository.
36
36
 
37
37
  ## Imports
38
38
 
@@ -44,7 +44,7 @@ import { colors as colorsDirect } from "@mrmeg/expo-ui/constants/colors";
44
44
  import { useResources, useTheme } from "@mrmeg/expo-ui/hooks";
45
45
  import { useTheme as useThemeDirect } from "@mrmeg/expo-ui/hooks/useTheme";
46
46
  import { globalUIStore, useThemeStore } from "@mrmeg/expo-ui/state";
47
- import { hapticLight } from "@mrmeg/expo-ui/lib";
47
+ import { configureExpoUiI18n, hapticLight } from "@mrmeg/expo-ui/lib";
48
48
  ```
49
49
 
50
50
  The root barrel also exports the public surface:
@@ -116,6 +116,18 @@ Useful `StyledText` props:
116
116
  - `variant`: `sansSerif`, `serif`
117
117
  - `align`, `tx`, `txOptions`
118
118
 
119
+ `tx` support is opt-in. Without a configured translator, `tx` renders its
120
+ fallback text when provided and otherwise renders the key; package-owned
121
+ defaults such as notification titles use readable fallback text. Consumers
122
+ that already use i18n can connect it once near app startup:
123
+
124
+ ```tsx
125
+ import { configureExpoUiI18n } from "@mrmeg/expo-ui/lib";
126
+ import { i18n } from "./i18n";
127
+
128
+ configureExpoUiI18n((key, options) => i18n.t(key, options));
129
+ ```
130
+
119
131
  ## Component Guide
120
132
 
121
133
  All components are exported from `@mrmeg/expo-ui/components`; direct imports such as `@mrmeg/expo-ui/components/Button` are supported. Use this table as the first stop before building a new primitive in a consumer app.
@@ -336,13 +348,14 @@ publishing instead:
336
348
  1. In npm package settings for `@mrmeg/expo-ui`, add a trusted publisher:
337
349
  GitHub Actions, owner `mrmeg`, repository `expo-template`, workflow
338
350
  filename `publish-ui.yml`.
339
- 2. In GitHub Actions, run the `Publish UI Package` workflow with `version=patch`
340
- and `ref=dev`.
351
+ 2. Bump `packages/ui/package.json` in a commit and push it to `main`.
341
352
 
342
- The workflow uses npm OIDC, not a checked-in token or local npm login. It bumps
343
- the package version, updates `bun.lock`, runs the package gates, lets npm CLI
344
- use the GitHub Actions OIDC environment, commits the version bump, and publishes
345
- from `packages/ui`.
353
+ The workflow uses npm OIDC, not a checked-in token or local npm login. On push,
354
+ it reads the committed package version, skips cleanly if that version is already
355
+ published, otherwise runs the package gates and publishes from `packages/ui`.
356
+ The workflow can also be run manually with `version=patch` and `ref=main`; manual
357
+ runs bump the package version, update `bun.lock`, run the package gates, commit
358
+ the version bump, and publish.
346
359
 
347
360
  Keep `repository.url` in `package.json` as
348
361
  `git+https://github.com/mrmeg/expo-template.git`. npm trusted publishing checks
@@ -353,9 +366,9 @@ If npm trusted publishing is blocked by package settings, add an npm automation
353
366
  or granular publish token to GitHub Actions secrets as `NPM_TOKEN` and rerun the
354
367
  same workflow. The token is used only for the publish step.
355
368
 
356
- If the workflow fails after the version is already bumped, rerun it with the
357
- exact current package version, for example `version=0.1.2`. Exact-version reruns
358
- do not bump again.
369
+ If the manual workflow fails after the version is already bumped, rerun it with
370
+ the exact current package version, for example `version=0.1.3`. Exact-version
371
+ reruns do not bump again.
359
372
 
360
373
  Manual package checks:
361
374
 
@@ -20,7 +20,7 @@ export interface ButtonProps extends PressableProps {
20
20
  */
21
21
  tx?: TextProps["tx"];
22
22
  /**
23
- * The text to display if not using `tx` or nested components.
23
+ * The text to display directly, or as fallback text when `tx` is provided.
24
24
  */
25
25
  text?: TextProps["text"];
26
26
  /**
@@ -147,13 +147,13 @@ export function Button(props) {
147
147
  },
148
148
  // Spread array styles from Slot to prevent nested arrays on web
149
149
  ...(Array.isArray(styleOverride) ? styleOverride : [styleOverride]),
150
- ], children: [!!LeftAccessory && !loading && (_jsx(LeftAccessory, { style: styles.leftAccessory, pressableState: state, disabled: isDisabled })), loading && (_jsx(ActivityIndicator, { size: "small", color: textColor, style: styles.loader })), (tx || text) ? (_jsx(StyledText, { style: [
150
+ ], children: [!!LeftAccessory && !loading && (_jsx(LeftAccessory, { style: styles.leftAccessory, pressableState: state, disabled: isDisabled })), loading && (_jsx(ActivityIndicator, { size: "small", color: textColor, style: styles.loader })), (tx || text) ? (_jsx(StyledText, { tx: tx, text: text, txOptions: txOptions, style: [
151
151
  styles.text,
152
152
  state.pressed && styles.pressedText,
153
153
  state.pressed && pressedTextStyleOverride,
154
154
  isDisabled && disabledTextStyleOverride,
155
155
  textStyleOverride,
156
- ], children: tx || text })) : !loading && children ? (
156
+ ] })) : !loading && children ? (
157
157
  // Wrap string children in StyledText to apply TextColorContext
158
158
  typeof children === "string" ? (_jsx(StyledText, { style: [
159
159
  styles.text,
@@ -2,13 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useContext, useEffect, useRef } from "react";
3
3
  import { StyleSheet, View, ActivityIndicator, Pressable, Platform } from "react-native";
4
4
  import Animated, { useSharedValue, useAnimatedStyle, withTiming, runOnJS, useReducedMotion, Easing, } from "react-native-reanimated";
5
- import { useTranslation } from "react-i18next";
6
5
  import { SafeAreaInsetsContext } from "react-native-safe-area-context";
7
6
  import { fontFamilies } from "../constants/fonts.js";
8
7
  import { Icon } from "./Icon.js";
9
8
  import { useTheme } from "../hooks/useTheme.js";
10
9
  import { spacing } from "../constants/spacing.js";
11
10
  import { StyledText } from "./StyledText.js";
11
+ import { translateText } from "../lib/i18n.js";
12
12
  import { globalUIStore } from "../state/globalUIStore.js";
13
13
  /**
14
14
  * Notification
@@ -36,7 +36,6 @@ import { globalUIStore } from "../state/globalUIStore.js";
36
36
  * ```
37
37
  */
38
38
  export const Notification = () => {
39
- const { t } = useTranslation();
40
39
  const { theme, getShadowStyle } = useTheme();
41
40
  const reduceMotion = useReducedMotion();
42
41
  const insets = useContext(SafeAreaInsetsContext);
@@ -140,11 +139,11 @@ export const Notification = () => {
140
139
  return alert.title;
141
140
  switch (alert?.type) {
142
141
  case "error":
143
- return t("notification.error");
142
+ return translateText("notification.error", "Error");
144
143
  case "success":
145
- return t("notification.success");
144
+ return translateText("notification.success", "Success");
146
145
  case "warning":
147
- return t("notification.warning");
146
+ return translateText("notification.warning", "Warning");
148
147
  case "info":
149
148
  return "";
150
149
  default:
@@ -53,7 +53,7 @@ export type TextProps = RNTextProps & {
53
53
  */
54
54
  tx?: string;
55
55
  /**
56
- * The text to display if not using `tx` or nested components.
56
+ * The text to display directly, or as fallback text when `tx` is provided.
57
57
  */
58
58
  text?: string;
59
59
  /**
@@ -101,7 +101,7 @@ export declare const StyledText: React.ForwardRefExoticComponent<RNTextProps & {
101
101
  */
102
102
  tx?: string;
103
103
  /**
104
- * The text to display if not using `tx` or nested components.
104
+ * The text to display directly, or as fallback text when `tx` is provided.
105
105
  */
106
106
  text?: string;
107
107
  /**
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { forwardRef } from "react";
3
3
  import { Text as RNText, StyleSheet } from "react-native";
4
- import { useTranslation } from "react-i18next";
5
4
  import { useTheme } from "../hooks/useTheme.js";
6
5
  import { fontFamilies } from "../constants/fonts.js";
6
+ import { translateText } from "../lib/i18n.js";
7
7
  /**
8
8
  * TextClassContext provides className context for nested text components
9
9
  * Used by @rn-primitives to apply consistent styling through the component tree
@@ -75,7 +75,6 @@ const getFontFamilyWeight = (weight) => {
75
75
  export const StyledText = forwardRef((props, ref) => {
76
76
  const { tx, text, txOptions, style, variant = "sansSerif", fontWeight, size, semantic, align, children, ...otherProps } = props;
77
77
  const { theme } = useTheme();
78
- const { t } = useTranslation();
79
78
  // Check if there's a color override from parent context (e.g., Button)
80
79
  const contextColor = React.useContext(TextColorContext);
81
80
  // Use context color if provided, otherwise use theme default
@@ -100,7 +99,7 @@ export const StyledText = forwardRef((props, ref) => {
100
99
  const styleHasFontSize = flatStyle && "fontSize" in flatStyle;
101
100
  const styleHasLineHeight = flatStyle && "lineHeight" in flatStyle;
102
101
  const resolvedLineHeight = styleHasFontSize && !styleHasLineHeight ? undefined : lineHeight;
103
- const i18nText = tx ? String(t(tx, txOptions)) : text;
102
+ const i18nText = translateText(tx, text, txOptions);
104
103
  const content = i18nText || children;
105
104
  return (_jsx(RNText, { ref: ref, style: [
106
105
  {
@@ -0,0 +1,3 @@
1
+ export type TranslateFn = (key: string, options?: object) => string | undefined;
2
+ export declare function configureExpoUiI18n(fn: TranslateFn | null): void;
3
+ export declare function translateText(key?: string, fallbackText?: string, options?: object): string | undefined;
@@ -0,0 +1,11 @@
1
+ let translate = null;
2
+ export function configureExpoUiI18n(fn) {
3
+ translate = fn;
4
+ }
5
+ export function translateText(key, fallbackText, options) {
6
+ if (!key)
7
+ return fallbackText;
8
+ if (!translate)
9
+ return fallbackText ?? key;
10
+ return translate(key, options) ?? fallbackText ?? key;
11
+ }
@@ -1,2 +1,3 @@
1
1
  export * from "./animations";
2
2
  export * from "./haptics";
3
+ export * from "./i18n";
package/dist/lib/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./animations.js";
2
2
  export * from "./haptics.js";
3
+ export * from "./i18n.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrmeg/expo-ui",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "description": "Reusable Expo and React Native UI primitives for MrMeg projects.",
6
6
  "keywords": [
@@ -105,9 +105,7 @@
105
105
  "expo": "~55.0.0",
106
106
  "expo-font": "~55.0.0",
107
107
  "expo-haptics": "~55.0.0",
108
- "i18next": ">=25.0.0 <27.0.0",
109
108
  "react": ">=19.2.0 <20.0.0",
110
- "react-i18next": ">=15.0.0 <18.0.0",
111
109
  "react-native": ">=0.83.0 <0.84.0",
112
110
  "react-native-gesture-handler": "~2.30.0",
113
111
  "react-native-reanimated": "~4.2.0",