@chipmobilesdk/rn-theme 1.0.1 → 1.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # @chipmobilesdk/rn-theme
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - One diagnostic stream: install a sink with `installSdkSink` from
8
+ `@chipmobilesdk/rn-core` and it receives what every package observes (events,
9
+ failures, warnings) as `SdkDiagnostic`s, redacted. Listeners are notified through
10
+ rn-core, so a listener that throws no longer stops the others nor fails the call that
11
+ notified, and its error is reported rather than swallowed. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
12
+
13
+ - rn-ads: a banner's auto-refresh is a new cycle, so its `loaded` and `impression`
14
+ events are no longer deduplicated away; the dedupe memory is bounded. A throwing
15
+ status, consent or placement listener is reported as an `internal_error` event.
16
+ - rn-i18n: new `onDiagnostic` config receives every diagnostic (`I18nDiagnostic`,
17
+ with a stable `code`); without it, warnings go to `console.warn` in development
18
+ builds only. The warn-once memory is bounded.
19
+ - rn-chart: now depends on rn-core for its diagnostics.
20
+ - rn-payment: status, entitlement and pending listeners report a throw to the logger
21
+ instead of swallowing it.
22
+
23
+ - The package's errors extend `SdkError` from the new `@chipmobilesdk/rn-core`
24
+ dependency: `isSdkError` recognises them alongside every other package's, and each
25
+ carries `pkg`, `code` and `retryable`. Names and codes are unchanged. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
26
+
27
+ - rn-ai and rn-camera: `retryable` is the new name of `recoverable`, which stays as a
28
+ deprecated alias.
29
+ - rn-auth: `retryable` is true for `network-unavailable`. `redact` also removes URLs,
30
+ e-mail addresses and UUIDs.
31
+ - rn-ads: `redact` also removes JWTs and e-mail addresses, and reduces an `AdsError`
32
+ to its code.
33
+ - rn-ai: an error message quoted from the gateway is pattern-redacted.
34
+ - rn-camera: `redact` also removes JWTs, e-mail addresses and UUIDs.
35
+ - rn-theme: `ThemeConfigError` has `code: 'invalid_config'` and `ThemeContrastError`
36
+ `code: 'contrast'` (new `ThemeErrorCode` type).
37
+ - rn-i18n: `I18nConfigError` has `code: 'invalid_config'` and `UnitConversionError`
38
+ `code: 'unit_conversion'` (new `I18nErrorCode` type).
39
+
40
+ - Native peers load through `lazyPeer` from `@chipmobilesdk/rn-core`, on first use,
41
+ and a missing one is each package's own typed error. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
42
+
43
+ - rn-auth: creating `auth` no longer loads react-native-app-auth or
44
+ react-native-keychain; each loads on first use. New entrypoints
45
+ `@chipmobilesdk/rn-auth/app-auth` and `/keychain`. The default platform comes from
46
+ rn-core.
47
+ - rn-camera: a preview whose VisionCamera feed cannot load fails the session with
48
+ `cameraUnavailable` (new `controller.reportSurfaceUnavailable()`), instead of
49
+ showing black with no error. A missing VisionCamera module is `cameraUnavailable`.
50
+ - rn-notification: `createMmkvStore` reports a fallback to memory as
51
+ `STATE_STORE_DEGRADED` (to the SDK sinks and a new `onDegraded` option) and
52
+ exposes `durable`.
53
+ - rn-payment: an iOS app validates its catalog against the iOS product ids; the
54
+ default platform used to be Android everywhere. A missing react-native-iap is
55
+ `PAYMENT_UNAVAILABLE`.
56
+ - rn-theme: `buildThemes` runs without React Native (new `hairlineWidth` option) and
57
+ importing the package no longer loads Unistyles; `applyThemeMode` loads it on
58
+ first use.
59
+ - rn-ads, rn-local-db: the same loading, with the same error codes as before.
60
+
61
+ ### Patch Changes
62
+
63
+ - Updated dependencies ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17), [#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17), [#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17)):
64
+ - @chipmobilesdk/rn-core@0.1.0
65
+
3
66
  ## 1.0.1
4
67
 
5
68
  ### Patch Changes
package/README.md CHANGED
@@ -90,6 +90,7 @@ applyThemeMode('dark'); // fixed; survives later OS scheme changes
90
90
 
91
91
  `applyThemeMode` switches adaptive themes off before setting a manual theme — in the other
92
92
  order the next OS change overrides the user's choice. Persisting the preference is the app's.
93
+ Unistyles is loaded the first time a mode is applied, never when the package is imported.
93
94
 
94
95
  ## Colour System
95
96
 
@@ -220,7 +221,10 @@ belong in `extend`.
220
221
 
221
222
  ## Integrations
222
223
 
223
- - `buildThemes(config)` — the same themes without Unistyles, e.g. for tests or other libraries.
224
+ - `buildThemes(config, options?)` — the same themes without Unistyles, e.g. for tests, other
225
+ libraries or a token export. It needs no React Native either: pass `platform` and
226
+ `hairlineWidth`, or they default to the running platform and
227
+ `StyleSheet.hairlineWidth` (1 where React Native is not loaded).
224
228
  - `toNavigationTheme(theme, scheme)` — a React Navigation theme (no navigation dependency).
225
229
 
226
230
  ## Public Runtime Exports
@@ -244,6 +248,10 @@ Only `@chipmobilesdk/rn-theme` is public. Deep imports (`.../src/...`) are unsup
244
248
 
245
249
  ## Configuration Validation
246
250
 
251
+ Both errors extend `SdkError` from `@chipmobilesdk/rn-core` and carry a `code`
252
+ (`ThemeErrorCode`): `invalid_config` for `ThemeConfigError`, `contrast` for
253
+ `ThemeContrastError`.
254
+
247
255
  Invalid input throws `ThemeConfigError` whose `path` names the field, e.g.
248
256
  `overrides.dark.color.error`. Checked: brand and seed colours (opaque, supported format),
249
257
  tone colours, typography numbers and font maps, named themes, override keys (must exist),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chipmobilesdk/rn-theme",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "main": "src/index.ts",
@@ -29,9 +29,12 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "scripts": {
32
- "typecheck": "tsc -p tsconfig.json --noEmit",
32
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json",
33
33
  "pack:check": "npm pack --dry-run"
34
34
  },
35
+ "dependencies": {
36
+ "@chipmobilesdk/rn-core": "^0.1.0"
37
+ },
35
38
  "peerDependencies": {
36
39
  "react": ">=19",
37
40
  "react-native": ">=0.85",
@@ -49,7 +49,8 @@ Unistyles 3. Single source of truth for visual tokens; app tokens join through `
49
49
 
50
50
  Runtime: `createUnistylesConfig`, `buildThemes`, `applyThemeMode`, `byTone`, `layout`,
51
51
  `presets`, `auditTheme`, `contrastRatio`, `withAlpha`, `toNavigationTheme`,
52
- `DEFAULT_BREAKPOINTS`, `ThemeConfigError`, `ThemeContrastError`.
52
+ `DEFAULT_BREAKPOINTS`, `ThemeConfigError`, `ThemeContrastError` (both rn-core SDK errors with a
53
+ `ThemeErrorCode`).
53
54
 
54
55
  Theme groups: `color`, `tone`, `typography`, `font`, `fontWeight`, `spacing`, `radius`,
55
56
  `borderWidth`, `size`, `opacity`, `motion`, `shadow`, `zIndex`, `maxFontScale`,
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  export { buildThemes } from './theme/buildThemes';
3
3
  export type { BuildThemesOptions } from './theme/buildThemes';
4
4
  export { ThemeConfigError, ThemeContrastError } from './theme/errors';
5
+ export type { ThemeErrorCode } from './theme/errors';
5
6
 
6
7
  // ─── Unistyles ──────────────────────────────────────────────────────────────
7
8
  export { createUnistylesConfig } from './unistyles/createUnistylesConfig';
@@ -1,4 +1,6 @@
1
- import { Platform as ReactNativePlatform } from 'react-native';
1
+ import { createDiagnosticChannel } from '@chipmobilesdk/rn-core';
2
+
3
+ import { currentPlatform, lazyPeer } from '@chipmobilesdk/rn-core';
2
4
  import { buildCategorical } from '../color/categorical';
3
5
  import { auditTheme } from '../color/contrast';
4
6
  import { buildColorRoles, buildPalettes, buildTones } from '../color/roles';
@@ -32,6 +34,26 @@ import { validateThemeConfig } from './validateThemeConfig';
32
34
  export interface BuildThemesOptions {
33
35
  /** Platform whose font resolution applies. Defaults to the running platform. */
34
36
  platform?: Platform;
37
+ /**
38
+ * `borderWidth.hairline`. Defaults to React Native's `StyleSheet.hairlineWidth`,
39
+ * or 1 where React Native is not loaded, so `buildThemes` runs on Node (THM-04).
40
+ */
41
+ hairlineWidth?: number;
42
+ }
43
+
44
+ /** Read only when no `hairlineWidth` is given, so building themes needs no React Native. */
45
+ const reactNative = lazyPeer('react-native', () => {
46
+ // Inside `try`, so Metro bundles an app that leaves this optional peer out.
47
+ try {
48
+ return require('react-native') as { StyleSheet?: { hairlineWidth?: number } };
49
+ } catch (cause) {
50
+ throw cause;
51
+ }
52
+ });
53
+
54
+ function deviceHairlineWidth(): number {
55
+ const result = reactNative.get();
56
+ return (result.ok ? result.module.StyleSheet?.hairlineWidth : undefined) ?? 1;
35
57
  }
36
58
 
37
59
  /** Groups that come straight from tokens (presets and tones are derived after). */
@@ -74,6 +96,7 @@ function buildOne(
74
96
  source: ThemeSource,
75
97
  fonts: ReturnType<typeof buildFonts>,
76
98
  overrides: unknown,
99
+ hairlineWidth: number,
77
100
  ): object {
78
101
  const { name, scheme, config, palettes } = source;
79
102
  const path = `overrides.${name}`;
@@ -86,7 +109,7 @@ function buildOne(
86
109
  fontWeight: buildFontWeight(),
87
110
  spacing: buildSpacing(),
88
111
  radius: buildRadius(),
89
- borderWidth: buildBorderWidth(),
112
+ borderWidth: buildBorderWidth(hairlineWidth),
90
113
  size: buildSize(),
91
114
  opacity: buildOpacity(),
92
115
  motion: buildMotion(),
@@ -129,11 +152,15 @@ function checkContrast(name: string, theme: object, strict: boolean): void {
129
152
  throw new ThemeContrastError(name, failures);
130
153
  }
131
154
 
155
+ channel.emit('contrast', 'warn', { theme: name, failures: failures.length });
132
156
  console.warn(
133
157
  `[rn-theme] Theme "${name}" breaks contrast guarantees after overrides:\n${describeFailures(failures)}`,
134
158
  );
135
159
  }
136
160
 
161
+ /** Contrast warnings also reach the app's SDK sinks (`installSdkSink`, CORE-08). */
162
+ const channel = createDiagnosticChannel<'contrast'>('rn-theme');
163
+
137
164
  /**
138
165
  * Builds every theme from brand config — pure, with no Unistyles dependency
139
166
  * (spec AC-9.4). `createUnistylesConfig` wraps this; call it directly to feed
@@ -150,7 +177,9 @@ export function buildThemes<
150
177
  validateThemeConfig(config);
151
178
 
152
179
  const loose = config as unknown as ThemeConfig<object, string, string>;
153
- const platform = options?.platform ?? (ReactNativePlatform.OS === 'ios' ? 'ios' : 'android');
180
+ // rn-core's one platform source (CORE-09); off-device, fonts resolve as on Android.
181
+ const platform = options?.platform ?? (currentPlatform() === 'ios' ? 'ios' : 'android');
182
+ const hairlineWidth = options?.hairlineWidth ?? deviceHairlineWidth();
154
183
  const fonts = buildFonts(loose.typography, platform);
155
184
  const basePalettes = buildPalettes(loose);
156
185
  const sources: ThemeSource[] = [
@@ -176,7 +205,7 @@ export function buildThemes<
176
205
 
177
206
  for (const source of sources) {
178
207
  const overrides = (loose.overrides as Record<string, unknown> | undefined)?.[source.name];
179
- const theme = buildOne(source, fonts, overrides);
208
+ const theme = buildOne(source, fonts, overrides, hairlineWidth);
180
209
 
181
210
  checkContrast(source.name, theme, loose.strict === true);
182
211
  themes[source.name] = theme;
@@ -1,23 +1,36 @@
1
+ import { SdkError } from '@chipmobilesdk/rn-core';
2
+
1
3
  import type { ContrastFailure } from '../color/contrast';
2
4
 
3
- /** Invalid configuration or override; `path` names the offending field. */
4
- export class ThemeConfigError extends Error {
5
+ /**
6
+ * The package's error codes (THM-09): `invalid_config` for a config or override
7
+ * the package rejects, `contrast` for a theme that breaks a contrast guarantee.
8
+ */
9
+ export type ThemeErrorCode = 'invalid_config' | 'contrast';
10
+
11
+ /**
12
+ * Invalid configuration or override; `path` names the offending field. An
13
+ * `SdkError` (`@chipmobilesdk/rn-core`) with `pkg: 'rn-theme'`.
14
+ */
15
+ export class ThemeConfigError extends SdkError<ThemeErrorCode> {
5
16
  readonly path: string;
6
17
 
7
18
  constructor(path: string, message: string) {
8
- super(`${path}: ${message}`);
19
+ super('rn-theme', 'invalid_config', { message: `${path}: ${message}` });
9
20
  this.name = 'ThemeConfigError';
10
21
  this.path = path;
11
22
  }
12
23
  }
13
24
 
14
25
  /** Thrown in `strict` mode when a theme breaks a contrast guarantee. */
15
- export class ThemeContrastError extends Error {
26
+ export class ThemeContrastError extends SdkError<ThemeErrorCode> {
16
27
  readonly themeName: string;
17
28
  readonly failures: ContrastFailure[];
18
29
 
19
30
  constructor(themeName: string, failures: ContrastFailure[]) {
20
- super(`Theme "${themeName}" breaks contrast guarantees:\n${describeFailures(failures)}`);
31
+ super('rn-theme', 'contrast', {
32
+ message: `Theme "${themeName}" breaks contrast guarantees:\n${describeFailures(failures)}`,
33
+ });
21
34
  this.name = 'ThemeContrastError';
22
35
  this.themeName = themeName;
23
36
  this.failures = failures;
@@ -1,4 +1,3 @@
1
- import { StyleSheet } from 'react-native';
2
1
  import { withAlpha } from '../color/parse';
3
2
  import type {
4
3
  BorderWidthTokens,
@@ -35,8 +34,9 @@ export function buildRadius(): RadiusTokens {
35
34
  return { none: 0, xs: 2, sm: 4, md: 8, lg: 16, xl: 24, full: 9999 };
36
35
  }
37
36
 
38
- export function buildBorderWidth(): BorderWidthTokens {
39
- return { hairline: StyleSheet.hairlineWidth, thin: 1, thick: 2 };
37
+ /** `hairline` is the device's thinnest line, resolved by `buildThemes` (THM-04). */
38
+ export function buildBorderWidth(hairline: number): BorderWidthTokens {
39
+ return { hairline, thin: 1, thick: 2 };
40
40
  }
41
41
 
42
42
  export function buildSize(): SizeTokens {
@@ -1,4 +1,6 @@
1
- import { UnistylesRuntime } from 'react-native-unistyles';
1
+ import { lazyPeer } from '@chipmobilesdk/rn-core';
2
+
3
+ import { ThemeConfigError } from '../theme/errors';
2
4
 
3
5
  export type ThemeMode = 'system' | 'light' | 'dark';
4
6
 
@@ -8,16 +10,37 @@ export interface ThemeModeRuntime {
8
10
  setTheme(name: string): void;
9
11
  }
10
12
 
11
- // The package cannot see the app's augmented `UnistylesThemes`, so `setTheme`
12
- // is typed as `never` here; the runtime accepts any configured theme name.
13
- const defaultRuntime = UnistylesRuntime as unknown as ThemeModeRuntime;
13
+ // Loaded when a mode is first applied, so importing the package (and building
14
+ // themes) never loads Unistyles (THM-04). The package cannot see the app's
15
+ // augmented `UnistylesThemes`, so the runtime is typed by the two calls it needs.
16
+ const unistyles = lazyPeer('react-native-unistyles', () => {
17
+ // Inside `try`, so Metro bundles an app that leaves this optional peer out.
18
+ try {
19
+ return require('react-native-unistyles') as { UnistylesRuntime: ThemeModeRuntime };
20
+ } catch (cause) {
21
+ throw cause;
22
+ }
23
+ });
24
+
25
+ function defaultRuntime(): ThemeModeRuntime {
26
+ return unistyles.require(
27
+ peer =>
28
+ new ThemeConfigError(
29
+ 'applyThemeMode',
30
+ `${peer} is not installed; pass a runtime or install it`,
31
+ ),
32
+ ).UnistylesRuntime;
33
+ }
14
34
 
15
35
  /**
16
36
  * Applies a light/dark/system preference. Adaptive themes are switched off
17
37
  * before a manual theme is set; in the other order the next OS colour-scheme
18
38
  * change overrides the user's choice. Persisting the preference is the app's.
19
39
  */
20
- export function applyThemeMode(mode: ThemeMode, runtime: ThemeModeRuntime = defaultRuntime): void {
40
+ export function applyThemeMode(
41
+ mode: ThemeMode,
42
+ runtime: ThemeModeRuntime = defaultRuntime(),
43
+ ): void {
21
44
  if (mode === 'system') {
22
45
  runtime.setAdaptiveThemes(true);
23
46
  return;