@chipmobilesdk/rn-theme 0.2.1 → 1.0.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 (42) hide show
  1. package/CHANGELOG.md +91 -23
  2. package/NOTICE +9 -0
  3. package/README.md +297 -154
  4. package/package.json +49 -50
  5. package/skills/sdk-rn-theme/SKILL.md +45 -30
  6. package/src/color/categorical.ts +33 -0
  7. package/src/color/contrast.ts +230 -0
  8. package/src/color/parse.ts +120 -0
  9. package/src/color/roles.ts +262 -0
  10. package/src/index.ts +23 -17
  11. package/src/integration/navigationTheme.ts +51 -0
  12. package/src/presets/byTone.ts +28 -0
  13. package/src/presets/layout.ts +38 -0
  14. package/src/presets/themePresets.ts +101 -0
  15. package/src/theme/buildThemes.ts +178 -0
  16. package/src/theme/errors.ts +34 -0
  17. package/src/theme/overrides.ts +142 -0
  18. package/src/theme/validateThemeConfig.ts +171 -0
  19. package/src/tokens/defaults.ts +103 -46
  20. package/src/tokens/typography.ts +144 -64
  21. package/src/types/brand.ts +16 -0
  22. package/src/types/colors.ts +80 -11
  23. package/src/types/index.ts +44 -5
  24. package/src/types/theme.ts +161 -0
  25. package/src/types/tokens.ts +47 -5
  26. package/src/types/typography.ts +46 -10
  27. package/src/unistyles/createUnistylesConfig.ts +165 -177
  28. package/src/unistyles/themeMode.ts +28 -0
  29. package/src/vendor/mcu/LICENSE +191 -0
  30. package/src/vendor/mcu/hct/cam16.ts +419 -0
  31. package/src/vendor/mcu/hct/hct.ts +216 -0
  32. package/src/vendor/mcu/hct/hct_solver.ts +557 -0
  33. package/src/vendor/mcu/hct/viewing_conditions.ts +121 -0
  34. package/src/vendor/mcu/index.ts +5 -0
  35. package/src/vendor/mcu/palettes/tonal_palette.ts +172 -0
  36. package/src/vendor/mcu/utils/color_utils.ts +302 -0
  37. package/src/vendor/mcu/utils/math_utils.ts +148 -0
  38. package/src/types/style.ts +0 -26
  39. package/src/unistyles/types.ts +0 -24
  40. package/src/unistyles/validateThemeConfig.ts +0 -154
  41. package/src/utils/colorGeneration.ts +0 -87
  42. package/src/utils/colorMath.ts +0 -55
package/CHANGELOG.md CHANGED
@@ -1,24 +1,92 @@
1
- # Changelog
2
-
3
- ## 0.2.0 - 2026-06-06
4
-
5
- ### Added
6
-
7
- - Documented single-entrypoint public API for reusable theme generation and token types.
8
- - Added consumer setup guidance for host dependencies, setup order, Android/iOS expectations, and theme-start strategy.
9
- - Added package validation coverage for theme generation, config validation, public API imports, and package manifest readiness.
1
+ # Changelog
2
+
3
+ ## 1.0.0 - 2026-10-17
4
+
5
+ Major release: accessible colour roles for any brand, a complete token set, type-safe
6
+ extension, and style presets. See `specs/014-rn-theme-v1/spec.md` in the source repository.
7
+
8
+ ### Added
9
+
10
+ - HCT tonal colour engine (vendored material-color-utilities v0.4.0, Apache-2.0): every role
11
+ is a fixed tone, so documented contrast pairs hold for any brand input.
12
+ - Full Material 3 role set: `onXContainer`, `onSurfaceVariant`, surface tiers, `inverse*`,
13
+ `outline`, `outlineVariant`, `scrim`, semantic container roles.
14
+ - `seeds` (neutral and semantic hues), custom `tones`, `tone.*` bundles and `byTone`.
15
+ - `extend` for app tokens, `themes` for extra named themes, validated `overrides`, `strict`.
16
+ - `buildThemes`, `auditTheme`, `contrastRatio`, `withAlpha`, `applyThemeMode`,
17
+ `toNavigationTheme`, `ThemeConfigError`, `ThemeContrastError`, `DEFAULT_BREAKPOINTS`.
18
+ - Tokens: typography Small/Large sizes, `font`/`fontWeight` with per-platform font files,
19
+ spacing `none`/`xxs`/`smPlus`/`mdPlus`/`xlPlus`/`xxxl`, radius `none`/`xs`, `borderWidth`,
20
+ `size`, `opacity`, `motion`, `maxFontScale`, `categorical`; default breakpoints.
21
+ - Presets: static `layout` and `presets`; theme `text`, `stack`, `inline`, `border`, `box`.
22
+
23
+ ### Changed
24
+
25
+ - Brand colours keep hue and chroma but take fixed role tones; the supplied hex is no
26
+ longer used as-is. Neutral surfaces carry a faint primary tint.
27
+ - `error`/`success`/`warning`/`info` are now readable as text (≥ 4.5:1) in both themes.
28
+ - Shadows are `boxShadow` strings (cross-platform) instead of iOS props + `elevation`.
29
+ - The package no longer augments `react-native-unistyles`; the app augments once from
30
+ `typeof config.themes`.
31
+ - `createUnistylesConfig` returns `breakpoints` and never invokes an `initialTheme` resolver.
32
+
33
+ ### Removed
34
+
35
+ - `generateLightPalette`, `generateDarkPalette`, `UnistylesThemeFromBrand`, `ThemeColors`,
36
+ `StyleTheme`, `StyleConfig`, the `tinycolor2` dependency, named and `hsl()` colour inputs.
37
+
38
+ ### Migration (0.2.x → 1.0.0)
39
+
40
+ | 0.2.x | 1.0.0 |
41
+ |---|---|
42
+ | `createUnistylesConfig({ brandColors, typographyConfig })` | `createUnistylesConfig({ brandColors, typography })` |
43
+ | `StyleConfig` / `StyleTheme` / `ThemeColors` | `ThemeConfig` / `Theme` / `ColorRoles` |
44
+ | `UnistylesThemeFromBrand` in `declare module` | `interface UnistylesThemes extends AppThemes {}` with `type AppThemes = typeof config.themes` |
45
+ | `generateLightPalette(brand)` / `generateDarkPalette(brand)` | `buildThemes({ brandColors }).light.color` / `.dark.color` |
46
+ | `color.border` | `color.outline` (interactive boundaries, ≥ 3:1) or `color.outlineVariant` (dividers) |
47
+ | `color.disabled` used as muted text | `color.onSurfaceVariant` or `text.bodyMuted` / `text.labelMuted` |
48
+ | `color.disabled` for inactive UI | unchanged meaning; now `onSurface` at 38 % opacity, contrast-exempt |
49
+ | `color.error`/`success`/`warning`/`info` as fills | `color.errorContainer` … with `onErrorContainer` …, or `tone.*.fill` / `onFill` |
50
+ | `color.primary` equal to the brand hex | tone-mapped; override `color.primary` to pin a hex |
51
+ | `shadow.md.shadowColor` … `elevation` | `shadow.md.boxShadow` |
52
+ | App tokens spread into the theme + cast | `extend: ({ theme }) => ({ ... })` |
53
+ | `overrides` adding unknown keys | rejected — use `extend` |
54
+ | Colour strings like `teal`, `hsl(...)` | `#RRGGBB`, `#RRGGBBAA`, `rgb()`, `rgba()` |
55
+ | Typography roles | unchanged sizes (medium); `<role>Small` / `<role>Large` added |
56
+ | Spacing / radius keys | unchanged values; new keys added |
57
+
58
+ ## 0.2.2 - 2026-08-02
59
+
60
+ ### Changed
61
+
62
+ - README and agent skill: Unistyles 3 style authoring and TypeScript diagnostics guidance.
63
+ No runtime change.
64
+
65
+ ## 0.2.1 - 2026-08-01
66
+
67
+ ### Changed
68
+
69
+ - The agent skill (`skills/`) ships in the package. No runtime change.
70
+
71
+ ## 0.2.0 - 2026-06-06
72
+
73
+ ### Added
74
+
75
+ - Documented single-entrypoint public API for reusable theme generation and token types.
76
+ - Added consumer setup guidance for host dependencies, setup order, Android/iOS expectations, and theme-start strategy.
77
+ - Added package validation coverage for theme generation, config validation, public API imports, and package manifest readiness.
10
78
  - Added package publish-readiness metadata and dry-run validation expectations.
11
-
12
- ### Changed
13
-
14
- - Classified host integrations as peer dependencies and kept runtime color generation in package dependencies.
15
- - Kept app-specific brand colors and typography outside the package.
16
-
17
- ### Migration
18
-
19
- - This is a backward-compatible minor release for existing workspace consumers importing from `@chipmobilesdk/rn-theme`.
20
- - Consumers importing private subpaths must migrate to the package root entrypoint.
21
-
22
- ### Deprecations
23
-
24
- - Private subpath imports are unsupported and should not be used by consuming apps.
79
+
80
+ ### Changed
81
+
82
+ - Classified host integrations as peer dependencies and kept runtime color generation in package dependencies.
83
+ - Kept app-specific brand colors and typography outside the package.
84
+
85
+ ### Migration
86
+
87
+ - This is a backward-compatible minor release for existing workspace consumers importing from `@chipmobilesdk/rn-theme`.
88
+ - Consumers importing private subpaths must migrate to the package root entrypoint.
89
+
90
+ ### Deprecations
91
+
92
+ - Private subpath imports are unsupported and should not be used by consuming apps.
package/NOTICE ADDED
@@ -0,0 +1,9 @@
1
+ @chipmobilesdk/rn-theme
2
+ Copyright ChipMobileSdk.
3
+
4
+ This package includes software from material-color-utilities
5
+ (https://github.com/material-foundation/material-color-utilities), v0.4.0,
6
+ commit dd62982dfdefdfc9232ce8d76e122fb1c6d188ae, Copyright 2021-2023 Google LLC,
7
+ licensed under the Apache License, Version 2.0. The vendored files live in
8
+ src/vendor/mcu/ together with a copy of that license (src/vendor/mcu/LICENSE);
9
+ each file states the modification made to it.
package/README.md CHANGED
@@ -1,159 +1,302 @@
1
- # @chipmobilesdk/rn-theme
2
-
3
- Reusable React Native theme generation and token contracts for ChipMobileSdk-family mobile apps.
1
+ # @chipmobilesdk/rn-theme
2
+
3
+ Accessible theme generation, design tokens and style presets for ChipMobileSdk-family React
4
+ Native apps, configured for `react-native-unistyles` 3.
4
5
 
5
6
  > **License:** `UNLICENSED`. Published publicly for use by the owner's
6
- > applications; no open-source license is granted.
7
+ > applications; no open-source license is granted. Includes Apache-2.0 code from
8
+ > material-color-utilities — see `NOTICE`.
7
9
 
8
- - **Version**: `0.2.0`
10
+ - **Version**: `1.0.0`
9
11
  - **Entrypoint**: `@chipmobilesdk/rn-theme`
10
12
  - **Demo**: `src/screens/ExampleScreen.tsx`
11
-
12
- ## Install
13
-
14
- Install the package from the public npm registry, then install the host styling dependencies in the consuming app:
15
-
16
- ```bash
17
- npm install @chipmobilesdk/rn-theme react-native-unistyles
18
- ```
19
-
20
- The host app owns `react`, `react-native`, and `react-native-unistyles`. If the app uses a safe-area shell like the ChipMobileSdk demo, install `react-native-safe-area-context` in the app as an app dependency. The theme package itself does not import safe-area APIs.
21
-
22
- ## Compatibility
23
-
24
- The current ChipMobileSdk demo validates against:
25
-
26
- - React `19.2.3`
27
- - React Native `0.85.3`
28
- - React Native Unistyles `^3.2.5`
29
- - TypeScript `^5.8.3`
30
- - Node.js `>=22.11.0`
31
- - Android and iOS host apps supported by the current React Native/Unistyles setup
32
-
33
- The package is TypeScript-only and does not include native Android or iOS code, permissions, entitlements, analytics, tracking, billing, storage, or network behavior.
34
-
35
- ## Setup Order
36
-
37
- Create app-owned brand values:
38
-
39
- ```ts
40
- import type { BrandColors, TypographyConfig } from '@chipmobilesdk/rn-theme';
41
-
42
- export const BRAND_COLORS: BrandColors = {
43
- primary: '#046b63',
44
- secondary: '#625b71',
45
- tertiary: '#7d5260',
46
- };
47
-
48
- export const BRAND_TYPOGRAPHY: TypographyConfig = {
49
- fontFamily: 'System',
50
- baseFontSize: 16,
51
- fontScale: 1,
52
- };
53
- ```
54
-
55
- Configure Unistyles before any themed component renders:
56
-
57
- ```ts
58
- import { StyleSheet } from 'react-native-unistyles';
59
- import { createUnistylesConfig } from '@chipmobilesdk/rn-theme';
60
- import type { UnistylesThemeFromBrand } from '@chipmobilesdk/rn-theme';
61
- import { BRAND_COLORS, BRAND_TYPOGRAPHY } from './brand';
62
-
63
- declare module 'react-native-unistyles' {
64
- export interface UnistylesThemes extends UnistylesThemeFromBrand {}
65
- }
66
-
67
- StyleSheet.configure(
68
- createUnistylesConfig(
69
- {
70
- brandColors: BRAND_COLORS,
71
- typographyConfig: BRAND_TYPOGRAPHY,
72
- },
73
- { initialTheme: 'dark' },
74
- ),
75
- );
76
- ```
77
-
78
- Import the setup module first in the app entrypoint.
79
-
80
- ## Theme Start Strategy
81
-
82
- Choose exactly one startup strategy.
83
-
84
- Manual initial theme:
85
-
86
- ```ts
87
- createUnistylesConfig(config, { initialTheme: 'light' });
88
- ```
89
-
90
- System adaptive themes:
91
-
92
- ```ts
93
- createUnistylesConfig(config, { adaptiveThemes: true });
94
- ```
95
-
96
- Using `initialTheme` and `adaptiveThemes: true` together is invalid. Persisting a user theme preference, if needed, belongs in the consuming app.
97
-
98
- ## Public Runtime Exports
99
-
100
- - `createUnistylesConfig(config, options?)`
101
- - `generateLightPalette(brandColors)`
102
- - `generateDarkPalette(brandColors)`
103
-
104
- ## Public Type Exports
105
-
106
- - `BrandColors`
107
- - `ThemeColors`
108
- - `TypographyConfig`
109
- - `TypographyToken`
110
- - `Typography`
111
- - `SpacingTokens`
112
- - `RadiusTokens`
113
- - `ShadowToken`
114
- - `ShadowTokens`
115
- - `ZIndexTokens`
116
- - `DeepPartial`
117
- - `StyleConfig`
118
- - `StyleTheme`
119
- - `UnistylesThemes`
120
- - `UnistylesConfigOptions`
121
- - `UnistylesConfig`
122
- - `UnistylesThemeFromBrand`
123
-
124
- ## Private Modules
125
-
126
- Only `@chipmobilesdk/rn-theme` is public. Imports from `@chipmobilesdk/rn-theme/src/...`, `@chipmobilesdk/rn-theme/utils/...`, `@chipmobilesdk/rn-theme/tokens/...`, `@chipmobilesdk/rn-theme/types/...`, or `@chipmobilesdk/rn-theme/unistyles/...` are unsupported.
127
-
128
- ## Configuration Validation
129
-
130
- The package validates required brand colors, positive typography values, valid color strings, and invalid theme-start combinations before returning a Unistyles config. Missing optional typography fields use documented defaults.
131
-
132
- ## Dependency Classification
133
-
134
- - `dependencies`: runtime implementation packages imported by package code, currently `tinycolor2`.
135
- - `peerDependencies`: host framework/integration packages supplied by each app: `react`, `react-native`, and `react-native-unistyles`.
136
- - `devDependencies`: package-local type and validation tooling such as `@types/tinycolor2`.
137
- - `react-native-safe-area-context`: app/demo dependency only, because the package does not import it.
138
-
139
- ## Release Validation
140
-
141
- Run from the repository root:
142
-
143
- ```bash
144
- npm run lint
145
- npm test
146
- npm run typecheck
147
- npm run typecheck:theme
148
- npm run pack:theme
149
- ```
150
-
151
- Record Android and iOS demo evidence under `specs/001-enhance-theme-package/validation/` before another app adopts the package.
152
-
153
- ## Semver and Migration
154
-
155
- - Patch: internal fix with no public shape or behavior change.
156
- - Minor: backward-compatible public additions or documentation/package metadata improvements.
157
- - Major: removed export, renamed token, changed token shape, changed default semantics, or changed required consumer setup.
158
-
159
- Breaking changes require migration notes and a deprecation decision before release.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @chipmobilesdk/rn-theme react-native-unistyles
18
+ ```
19
+
20
+ The host app owns `react`, `react-native` and `react-native-unistyles`. The package has no
21
+ runtime dependency, no native code, and renders no components.
22
+
23
+ ## Compatibility
24
+
25
+ Validated in the ChipMobileSdk demo against React `19.2.3`, React Native `0.85.3`,
26
+ React Native Unistyles `^3.2.5`, TypeScript `^5.8.3` and Node.js `>=22.11.0`, on Android and
27
+ iOS. Shadows use React Native's cross-platform `boxShadow` (New Architecture).
28
+
29
+ ## Setup Order
30
+
31
+ App-owned brand values (`src/theme/brand.ts`):
32
+
33
+ ```ts
34
+ import type { BrandColors, TypographyConfig } from '@chipmobilesdk/rn-theme';
35
+
36
+ export const BRAND_COLORS: BrandColors = {
37
+ primary: '#046b63',
38
+ secondary: '#625b71',
39
+ tertiary: '#7d5260',
40
+ };
41
+
42
+ export const BRAND_TYPOGRAPHY: TypographyConfig = { fontFamily: 'System' };
43
+ ```
44
+
45
+ Configure Unistyles before any themed component renders, and teach Unistyles the theme
46
+ shape **once**, from the config itself:
47
+
48
+ ```ts
49
+ import { StyleSheet } from 'react-native-unistyles';
50
+ import { createUnistylesConfig } from '@chipmobilesdk/rn-theme';
51
+ import { BRAND_COLORS, BRAND_TYPOGRAPHY } from './brand';
52
+
53
+ const unistylesConfig = createUnistylesConfig(
54
+ { brandColors: BRAND_COLORS, typography: BRAND_TYPOGRAPHY },
55
+ { initialTheme: 'light' },
56
+ );
57
+
58
+ type AppThemes = typeof unistylesConfig.themes;
59
+ type AppBreakpoints = typeof unistylesConfig.breakpoints;
60
+
61
+ declare module 'react-native-unistyles' {
62
+ export interface UnistylesThemes extends AppThemes {}
63
+ export interface UnistylesBreakpoints extends AppBreakpoints {}
64
+ }
65
+
66
+ StyleSheet.configure(unistylesConfig);
67
+ ```
68
+
69
+ Import that module first in the app entrypoint. Pass the config **inline** (or with
70
+ `satisfies ThemeConfig`): annotating a variable as `ThemeConfig` erases the extension, tone
71
+ and theme-name types the config would otherwise carry.
72
+
73
+ The package does not augment `react-native-unistyles` itself — your augmentation is the
74
+ only one, so an extended theme shape never conflicts.
75
+
76
+ ## Theme Start Strategy
77
+
78
+ Choose one: `{ initialTheme: 'light' | 'dark' | <named theme> }` or
79
+ `{ adaptiveThemes: true }`. Both together are rejected. A resolver function for
80
+ `initialTheme` is passed to Unistyles untouched — the package never calls it.
81
+
82
+ To apply a stored preference at runtime:
83
+
84
+ ```ts
85
+ import { applyThemeMode } from '@chipmobilesdk/rn-theme';
86
+
87
+ applyThemeMode('system'); // follow the OS
88
+ applyThemeMode('dark'); // fixed; survives later OS scheme changes
89
+ ```
90
+
91
+ `applyThemeMode` switches adaptive themes off before setting a manual theme — in the other
92
+ order the next OS change overrides the user's choice. Persisting the preference is the app's.
93
+
94
+ ## Colour System
95
+
96
+ Every role is a fixed tone of an HCT tonal palette built from a brand or seed colour, so
97
+ contrast depends only on tone and holds for **any** brand:
98
+
99
+ | Guarantee | Pairs |
100
+ |---|---|
101
+ | ≥ 4.5:1 (text) | `error`/`success`/`warning`/`info` and `onSurfaceVariant` on `background` and `surface`; every `onX` on `X`; `onXContainer` on `XContainer`; `onSurface` on each surface tier; tone `fg`, `onContainer`, `onFill`; every text preset except `text.disabled` |
102
+ | ≥ 3:1 (non-text) | `outline` on `background`/`surface`; tone `border` on `background`; each `categorical` colour on `surface` |
103
+
104
+ - Brand colours keep their hue and chroma but always take the role tone (light 40 / dark 80;
105
+ containers 90 / 30). The supplied hex is not used as-is — override a role if a screen
106
+ needs the exact brand hex, and the contrast check below still applies.
107
+ - Neutrals carry a faint tint of the primary hue unless `seeds.neutral` is given. Semantic
108
+ hues come from `seeds.error|success|warning|info` (defaults: the 0.2.x light values).
109
+ - `disabled` is a **state** colour, exempt from contrast (WCAG 1.4.3). Muted readable text
110
+ is `onSurfaceVariant` or the `text.*Muted` presets.
111
+ - `outline` bounds interactive components; `outlineVariant` is for dividers.
112
+ - `auditTheme(theme)` lists every guaranteed pair below target. After overrides,
113
+ `buildThemes` warns in development; `strict: true` throws `ThemeContrastError` instead.
114
+
115
+ Accepted colours: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`, `rgb()`, `rgba()`. Brand and seed
116
+ colours must be opaque. `withAlpha(color, opacity)` applies an opacity token.
117
+
118
+ ## Tokens
119
+
120
+ | Group | Contents |
121
+ |---|---|
122
+ | `color` | Material 3 roles (above) plus `scrim`, `shadow`, `disabled` |
123
+ | `tone` | `neutral`, `primary`, `info`, `success`, `warning`, `error` and custom tones — each `{ fg, container, onContainer, border, fill, onFill }` |
124
+ | `typography` | `display`, `headline`, `title`, `body`, `label`, `caption` (medium) plus `<role>Small` / `<role>Large` |
125
+ | `font`, `fontWeight` | `regular`, `medium`, `semibold`, `bold`, `heavy` — `font.X` is `{ fontFamily, fontWeight }` resolved for the platform |
126
+ | `spacing` | `none` 0, `xxs` 2, `xs` 4, `sm` 8, `smPlus` 12, `md` 16, `mdPlus` 20, `lg` 24, `xl` 32, `xlPlus` 40, `xxl` 48, `xxxl` 64 |
127
+ | `radius` | `none`, `xs`, `sm`, `md`, `lg`, `xl`, `full` |
128
+ | `borderWidth` | `hairline`, `thin` 1, `thick` 2 |
129
+ | `size` | `touchTarget` 44, `icon.sm/md/lg` 16/20/24 |
130
+ | `opacity` | `hover`, `focus`, `pressed`, `dragged`, `disabledContent`, `disabledContainer` |
131
+ | `motion` | `duration.short/medium/long`, `easing.standard/emphasized/decelerate/accelerate` (`[x1, y1, x2, y2]`) |
132
+ | `shadow` | `sm`, `md`, `lg` — `{ boxShadow }` |
133
+ | `zIndex`, `maxFontScale`, `categorical` | layers; OS text-scale ceiling (default 2); chart series colours |
134
+
135
+ Breakpoints default to `{ compact: 0, medium: 600, expanded: 840 }`; replace them with the
136
+ `breakpoints` option.
137
+
138
+ ### Custom fonts
139
+
140
+ Android cannot pick a weight inside a family, so give each weight its file there:
141
+
142
+ ```ts
143
+ typography: {
144
+ fonts: {
145
+ family: 'Inter',
146
+ android: { regular: 'Inter-Regular', medium: 'Inter-Medium', semibold: 'Inter-SemiBold', bold: 'Inter-Bold' },
147
+ },
148
+ }
149
+ ```
150
+
151
+ Use `...theme.font.bold` (not a literal `fontWeight`) to change weight in a style. A missing
152
+ Android weight falls back to the nearest one with a development warning. On iOS a partial
153
+ map only renames the weights it lists.
154
+
155
+ ### Font scale
156
+
157
+ `fontScale` is a design-time multiplier and does not track the OS text size. Cap the OS
158
+ multiplier on your shared `Text` with `maxFontSizeMultiplier={theme.maxFontScale}`.
159
+
160
+ ## Style Presets
161
+
162
+ Presets are plain style data, so stylesheets keep only what is specific to them.
163
+
164
+ - **Static** (`layout`, `presets`) — no theme dependency, so Unistyles never recalculates them:
165
+ `layout.row`, `column`, `rowCenter`, `rowBetween`, `center`, `fill`, `wrap`, `absoluteFill`,
166
+ `selfStart`, `selfCenter`, `selfEnd`; `presets.glyphAlign` (Android icon-glyph centring),
167
+ `presets.tabularNums`.
168
+ - **Theme** — read tokens and follow overrides: `theme.text.*` (typography + AA colour),
169
+ `theme.stack.<spacing>` / `theme.inline.<spacing>`, `theme.border.hairline|top|bottom|all`,
170
+ `theme.box.outlined|card|sheet|touchTarget`.
171
+
172
+ ```ts
173
+ const styles = StyleSheet.create(theme => ({
174
+ row: { ...layout.rowCenter, ...theme.border.bottom, gap: theme.spacing.smPlus },
175
+ hint: theme.text.labelMuted,
176
+ container: {
177
+ ...theme.box.outlined,
178
+ variants: { tone: byTone(theme, t => ({ borderColor: t.border, backgroundColor: t.container })) },
179
+ },
180
+ // One variant style for every text; combine it by array.
181
+ toneText: { variants: { tone: byTone(theme, t => ({ color: t.onContainer })) } },
182
+ title: theme.typography.body,
183
+ }));
184
+
185
+ // <Text style={[styles.title, styles.toneText]} />
186
+ ```
187
+
188
+ ### Rules the Unistyles Babel plugin imposes
189
+
190
+ The plugin decides **statically** which styles update on theme or variant changes. Verified
191
+ against Unistyles 3.2.5 and 3.3.0:
192
+
193
+ 1. Read the theme through the stylesheet's own parameter. `const theme = cast(raw)` inside
194
+ the factory hides **every** dependency — those styles stop updating on theme change. Add
195
+ app tokens with `extend` instead of casting.
196
+ 2. Keep a literal `variants` key in the style and call `byTone(theme, …)` inline under it. A
197
+ variants object built elsewhere, or spread in, loses the theme or variant dependency.
198
+
199
+ ## Extending the Theme
200
+
201
+ Add app tokens with `extend`; they are typed everywhere without casts, overridable, and
202
+ validated (a value that is a colour must stay a colour):
203
+
204
+ ```ts
205
+ createUnistylesConfig({
206
+ brandColors: BRAND_COLORS,
207
+ tones: { accent: '#BD260F' }, // full tone bundle, offered by byTone
208
+ extend: ({ theme, scheme }) => ({
209
+ appColor: { textMuted: theme.color.onSurfaceVariant, skeleton: theme.color.surfaceContainerHigh },
210
+ }),
211
+ themes: { brandB: { scheme: 'light', brandColors: BRAND_B } }, // extra named theme
212
+ overrides: {
213
+ light: { radius: { sm: 0, md: 0, lg: 0, xl: 0 }, appColor: { textMuted: '#706966' } },
214
+ },
215
+ });
216
+ ```
217
+
218
+ Extensions may not reuse a base group name. Override keys must already exist — new tokens
219
+ belong in `extend`.
220
+
221
+ ## Integrations
222
+
223
+ - `buildThemes(config)` — the same themes without Unistyles, e.g. for tests or other libraries.
224
+ - `toNavigationTheme(theme, scheme)` — a React Navigation theme (no navigation dependency).
225
+
226
+ ## Public Runtime Exports
227
+
228
+ `buildThemes`, `createUnistylesConfig`, `applyThemeMode`, `auditTheme`, `contrastRatio`,
229
+ `withAlpha`, `byTone`, `layout`, `presets`, `toNavigationTheme`, `DEFAULT_BREAKPOINTS`,
230
+ `ThemeConfigError`, `ThemeContrastError`.
231
+
232
+ ## Public Type Exports
233
+
234
+ Config: `ThemeConfig`, `BrandColors`, `SeedColors`, `TypographyConfig`, `FontConfig`,
235
+ `NamedThemeConfig`, `ExtendContext`, `UnistylesConfigOptions`, `BuildThemesOptions`.
236
+ Output: `Theme`, `BaseTheme`, `Themes`, `ThemeName`, `UnistylesConfig`, `ColorRoles`,
237
+ `ToneColors`, `Typography`, `TypographyToken`, `FontToken`, token and preset types
238
+ (`SpacingTokens`, `TextPresets`, `BoxPresets`, …), `ContrastFailure`, `NavigationTheme`,
239
+ `ThemeMode`, `ThemeModeRuntime`, `DeepPartial`.
240
+
241
+ ## Private Modules
242
+
243
+ Only `@chipmobilesdk/rn-theme` is public. Deep imports (`.../src/...`) are unsupported.
244
+
245
+ ## Configuration Validation
246
+
247
+ Invalid input throws `ThemeConfigError` whose `path` names the field, e.g.
248
+ `overrides.dark.color.error`. Checked: brand and seed colours (opaque, supported format),
249
+ tone colours, typography numbers and font maps, named themes, override keys (must exist),
250
+ override colours and numbers, Unistyles options and breakpoints.
251
+
252
+ ## Dependency Classification
253
+
254
+ - `dependencies`: none. HCT colour maths is vendored under `src/vendor/mcu` (Apache-2.0).
255
+ - `peerDependencies`: `react`, `react-native`, `react-native-unistyles`.
256
+
257
+ ## Unistyles 3 Style Authoring and TypeScript Diagnostics
258
+
259
+ ### Style arrays and variants solve different problems
260
+
261
+ Merge independent styles with array syntax (`[styles.container, styles.emphasis]`) — the
262
+ array keeps merge order and each style's Unistyles state. Never object-spread Unistyles
263
+ entries. Use `styles.useVariants(...)` for finite component states such as `selected` or
264
+ `tone`.
265
+
266
+ ### Dynamic functions and inference
267
+
268
+ Call a dynamic entry before passing it to `style`, with serializable arguments. If
269
+ TypeScript widens sibling entries to `UnistylesValues | ((...) => UnistylesValues)`, move the
270
+ dynamic entries into a separate `StyleSheet.create`. Every token and preset the package
271
+ exports is typed as React Native `TextStyle`/`ViewStyle` members, so spreading them never
272
+ degrades sibling inference.
273
+
274
+ ### Localize vague TypeScript errors
275
+
276
+ When an error is reported at every JSX use, move the stylesheet above the component or split
277
+ the suspect entries out to find the failing initializer.
278
+
279
+ See the Unistyles 3 guides on [merging styles](https://www.unistyl.es/v3/guides/merging-styles/),
280
+ [variants](https://www.unistyl.es/v3/references/variants/) and
281
+ [dynamic functions](https://www.unistyl.es/v3/references/dynamic-functions/).
282
+
283
+ ## Migrating from 0.2.x
284
+
285
+ See the mapping table in `CHANGELOG.md` (`## 1.0.0`). In short: rename `typographyConfig` to
286
+ `typography`; replace `color.border` with `outline` (interactive) or `outlineVariant`
287
+ (dividers); use `onSurfaceVariant` for muted text instead of `disabled`; replace the package
288
+ augmentation (`UnistylesThemeFromBrand`) with the one in *Setup Order*; move app tokens from
289
+ spreads and casts into `extend`.
290
+
291
+ ## Release Validation
292
+
293
+ ```bash
294
+ npm run validate:theme
295
+ ```
296
+
297
+ ## Semver and Migration
298
+
299
+ - Patch: internal fix with no public shape or behaviour change.
300
+ - Minor: backward-compatible additions.
301
+ - Major: removed export, renamed token, changed token shape or default values, or changed
302
+ consumer setup — with a migration table in the changelog.