@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 +63 -0
- package/README.md +9 -1
- package/package.json +5 -2
- package/skills/sdk-rn-theme/SKILL.md +2 -1
- package/src/index.ts +1 -0
- package/src/theme/buildThemes.ts +33 -4
- package/src/theme/errors.ts +18 -5
- package/src/tokens/defaults.ts +3 -3
- package/src/unistyles/themeMode.ts +28 -5
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
|
|
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
|
|
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
|
|
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';
|
package/src/theme/buildThemes.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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;
|
package/src/theme/errors.ts
CHANGED
|
@@ -1,23 +1,36 @@
|
|
|
1
|
+
import { SdkError } from '@chipmobilesdk/rn-core';
|
|
2
|
+
|
|
1
3
|
import type { ContrastFailure } from '../color/contrast';
|
|
2
4
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
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
|
|
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(
|
|
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;
|
package/src/tokens/defaults.ts
CHANGED
|
@@ -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
|
-
|
|
39
|
-
|
|
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 {
|
|
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
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
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(
|
|
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;
|