$npx -y skills add Code-with-Beto/skills --skill themingScaffold a unified, cross-platform color theme system into an Expo Router app. Sets up semantic system colors (resolved from native iOS labels and Android Material You dynamic colors), brand colors, automatic dark/light re-rendering, the React Navigation ThemeProvider, and a useC
| 1 | # Expo Unified Theming |
| 2 | |
| 3 | Set up one color system that works on iOS and Android at once. Instead of |
| 4 | sprinkling `Color.ios.*` and `Color.android.dynamic.*` (or hardcoded hex) all |
| 5 | over the app, the user gets semantic tokens (`background`, `text`, `link`, ...) |
| 6 | that resolve to the right native color per platform, flip with dark/light |
| 7 | automatically, and are read through a single `useColors()` hook. |
| 8 | |
| 9 | This is the pattern from the **Platano** template, packaged so it drops into any |
| 10 | Expo Router app. |
| 11 | |
| 12 | ## What gets installed |
| 13 | |
| 14 | Three files (bundled in `assets/`, adapt them to the project): |
| 15 | |
| 16 | - `config.ts` — the single source of truth. Semantic system tokens set to |
| 17 | `"native"` or a hex, plus brand colors. This is the only file the user edits |
| 18 | day to day. |
| 19 | - `colors.ts` — resolves tokens to real colors per platform, exposes |
| 20 | `getSystemColor`, `useSystemColors`, the brand helpers, and |
| 21 | `getNavigationTheme`. |
| 22 | - `ThemeContext.tsx` — `ColorsProvider` + the `useColors()` / `useTheme()` / |
| 23 | `useBrand()` hooks built on React's `use`. |
| 24 | |
| 25 | Plus a small edit to the root `_layout.tsx` to wire the providers. |
| 26 | |
| 27 | ## The two ideas that make this work |
| 28 | |
| 29 | Explain these to the user, because they are the whole point and the two things |
| 30 | people get wrong: |
| 31 | |
| 32 | 1. **Re-render on theme change.** Native and Material You colors are platform |
| 33 | color objects, not strings React can diff. The signal that the theme changed |
| 34 | is `useColorScheme()`. So the resolvers take the color scheme as an argument |
| 35 | even when they do not read it: that makes dark/light an explicit dependency, |
| 36 | and the value recomputes when the user toggles. The `ColorsProvider` reads |
| 37 | `useColorScheme()` once near the root, so a toggle re-renders every consumer. |
| 38 | |
| 39 | 2. **Two ThemeProviders, different jobs.** `ColorsProvider` (this skill) feeds |
| 40 | `useColors()` to your components. The navigation `ThemeProvider` themes the |
| 41 | navigation chrome (headers, tab bars, default background). Both live at the |
| 42 | root. Wire navigation's with `getNavigationTheme(dark)` so the chrome matches |
| 43 | your tokens. Do not confuse them or drop one. On **Expo SDK 56+** the |
| 44 | navigation theme bits (`ThemeProvider`, `DarkTheme`, `DefaultTheme`, `Theme`) |
| 45 | import from `expo-router/react-navigation`; on **older SDKs** they come from |
| 46 | `@react-navigation/native` instead (see Step 1). |
| 47 | |
| 48 | ## Step 1: Confirm the project, SDK, and the native Color API |
| 49 | |
| 50 | This skill targets **Expo Router** apps on **Expo SDK 56 or newer**. SDK 56 is |
| 51 | where the `Color` API (iOS system colors + Android Material You) and the |
| 52 | `expo-router/react-navigation` re-exports the templates use both ship. Check the |
| 53 | SDK and the `Color` API before relying on `"native"` tokens. |
| 54 | |
| 55 | ```bash |
| 56 | # Find the project's app dir and root layout (root-level or under src/) |
| 57 | ls app/_layout.tsx src/app/_layout.tsx 2>/dev/null |
| 58 | |
| 59 | # Check the installed Expo SDK major version |
| 60 | node -e "console.log('expo', require('expo/package.json').version)" 2>/dev/null |
| 61 | |
| 62 | # Confirm expo-router exports the Color API |
| 63 | node -e "console.log('Color export:', !!require('expo-router').Color)" 2>/dev/null |
| 64 | grep -rl "android.dynamic\|ios.systemBackground" node_modules/expo-router/build 2>/dev/null | head -1 |
| 65 | ``` |
| 66 | |
| 67 | Branch on what you find: |
| 68 | |
| 69 | - **SDK 56+ (target):** use the templates as written. Navigation theme bits |
| 70 | import from `expo-router/react-navigation`. |
| 71 | - **Older than SDK 56:** two adjustments are needed. |
| 72 | 1. Import `ThemeProvider`, `DarkTheme`, `DefaultTheme`, and `type Theme` from |
| 73 | `@react-navigation/native` instead of `expo-router/react-navigation` — in |
| 74 | both `colors.ts` and `_layout.tsx`. (Older Expo Router did not re-export |
| 75 | them, so you go to React Navigation directly.) |
| 76 | 2. The `Color` API likely is not present. Either recommend upgrading to SDK 56, |
| 77 | or use the **hex fallback**: keep the files but set the `system` tokens in |
| 78 | `config.ts` to hex values instead of `"native"` (no Material You, but |
| 79 | everything else works). `getBrandColors` still works; `isAndroidDynamic` |
| 80 | resolves to false without `Color`. |
| 81 | |
| 82 | Also detect the project's import alias from `tsconfig.json` (commonly `@/*` → |
| 83 | `src/*` or the project root). Match that alias when you write imports. If there |
| 84 | is no alias, use relative paths. |
| 85 | |
| 86 | ## Step 2: Decide where the files live |
| 87 | |
| 88 | Put the three files together in one folder so their relative imports |
| 89 | (`./config`, `./colors`) just work. Mirror the project's layout: |
| 90 | |
| 91 | - App under `src/app/` → create `src/theme/`. |
| 92 | - App a |