- The theme decides neutral colors: backgrounds, text, hairlines, status colors. It has a dark and a light palette.
- The brand decides the accent color and identity: the studio’s primary color, logo and name. It comes from the server per studio.
useColors() for neutrals and useBrandPalette() for the accent.
Tokens
src/theme/index.ts re-exports everything.
Spacing, radius, sizes
Fromsrc/theme/spacing.ts:
spacing.screen is the horizontal page gutter. sizes.maxContentWidth caps the content column, which keeps the layout sane in the web preview and on wide devices.
Typography
src/theme/typography.ts defines twelve variants. AppText takes one through its variant prop.
variantColorKey maps each variant to ink, inkSecondary or inkTertiary. AppText resolves that key against the active palette, so text recolors with the theme. The colors baked into the typography objects are a static fallback only.
Fonts
fontFamily is resolved once at module load from the stored language:
The root layout also loads Heebo and Assistant for the story share cards, Rubik for the glass home skin, Karantina and Assistant for the poster skin, and Varela Round for the bento skin.
Because the family is picked at module load, a language change needs the reload that
setLanguage performs.
Palettes
src/theme/colors.ts exports darkColors and lightColors with identical keys, typed as ThemePalette.
The dark background is
#0B0B14, which matches the native splash and backgroundColor in app.json. The light background is #F4F6F9.
colors is a static export of the dark palette for the few non-reactive uses. Components should use the hook.
Theme store
useThemeStore in src/theme/themeStore.ts holds scheme, the matching colors object and hasChoice.
Who wins
- The trainee’s choice. Set by the Dark mode switch on the profile tab, which calls
toggle(). - The studio default.
authStore.applyStudioSchemecallsapplyDefaultwhenstudio.branding.appThemeis'dark'or'light'. - Dark.
The app does not follow the operating system setting.
userInterfaceStyle in app.json is dark.
The root layout hydrates the theme before the auth store, and sets the status bar and Android navigation bar style from the scheme.
Themed styles
StaticStyleSheet.create cannot react to the scheme. Use useThemedStyles with a factory declared outside the component:
darkColors directly and do not follow the theme.
Studio brand
src/features/branding/brandStore.ts holds a Brand and a derived BrandPalette.
Where it comes from
authStore.brandFromStudio(studio) maps studio.branding from /v1/trainee/me and from the sign in response, then calls useBrandStore.apply(). This runs on sign in, on studio switch and on every refreshSession(). A coach who changes branding in the dashboard sees it in the app the next time the trainee opens it.
Mapping rules worth knowing:
logoUrlfalls back toiconUrl.storyBackgroundUrlfalls back to the olderselfieExampleUrlfield.primaryColormust be a six digit hex. Anything else falls back to the default red.homeThememust be one ofHOME_THEMES. Anything else becomesclassic.
apply() persists the normalized brand under perform_brand. hydrate() restores it at launch so the login screen is already in the studio’s colors. reset() runs on sign out.
Derived palette
paletteFor(primaryColor) builds:
Color helpers
src/theme/brandColor.ts:
useAuthAccent() uses ensureVibrant so a very dark studio color is replaced by #FF5F4D on the always-dark login screens.
Home skins
HOME_THEMES is ['classic', 'glass', 'poster', 'bento']. The coach picks one for the studio in the dashboard branding settings, and HomeScreen switches on useHomeTheme().
The skin also dresses every other screen. Screen renders SkinBackground behind its content whenever the skin is not classic:
Base colors per skin and scheme are in
SKIN_BASE_COLOR. The comment in SkinBackground.tsx notes that these recipes mirror the skin home screens and must change together.
Brand in the header and widgets
BrandTopBar shows the page title and the studio logo. If the logo fails to load it falls back to the studio name in the brand color.
widgetBrandLogo.ts downloads the studio logo into the shared widgets directory as brand-logo.png so the iOS widgets and Live Activity can show it. With no studio logo it installs the bundled Perform mark instead.
Rules for new UI
- Neutrals from
useColors(). Accent fromuseBrandPalette(). No hard-coded hex for either. - Spacing and radius from the tokens.
- Text through
AppTextwith a variant. - Styles that depend on the palette through
useThemedStyles. - The project rule is no shadows.
ProgressBarhas an optional glow, which is the one deliberate exception in the kit.