@react-native-rethemed/core 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +85 -2
- package/dist/cjs/color-mode-store.d.ts +32 -0
- package/dist/cjs/color-mode-store.js +64 -0
- package/dist/cjs/config.d.ts +14 -0
- package/dist/cjs/config.js +35 -0
- package/dist/cjs/create-themed-styles.d.ts +13 -0
- package/dist/cjs/create-themed-styles.js +37 -0
- package/dist/cjs/create-themed.d.ts +46 -0
- package/dist/cjs/create-themed.js +118 -0
- package/dist/cjs/define-theme.d.ts +11 -0
- package/dist/cjs/define-theme.js +15 -0
- package/dist/cjs/extend-theme.d.ts +14 -0
- package/dist/cjs/extend-theme.js +118 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.js +22 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/resolvers/color-resolver.d.ts +10 -0
- package/dist/cjs/resolvers/color-resolver.js +26 -0
- package/dist/cjs/resolvers/line-height-resolver.d.ts +8 -0
- package/dist/cjs/resolvers/line-height-resolver.js +20 -0
- package/dist/cjs/resolvers/style-resolver.d.ts +21 -0
- package/dist/cjs/resolvers/style-resolver.js +104 -0
- package/dist/cjs/semantic-colors.d.ts +14 -0
- package/dist/cjs/semantic-colors.js +32 -0
- package/dist/cjs/style-props.d.ts +28 -0
- package/dist/cjs/style-props.js +95 -0
- package/dist/cjs/text-tree.d.ts +30 -0
- package/dist/cjs/text-tree.js +79 -0
- package/dist/cjs/text-variants.d.ts +22 -0
- package/dist/cjs/text-variants.js +29 -0
- package/dist/cjs/types.d.ts +281 -0
- package/dist/cjs/types.js +2 -0
- package/dist/esm/color-mode-store.d.ts +32 -0
- package/dist/esm/color-mode-store.js +59 -0
- package/dist/esm/config.d.ts +14 -0
- package/dist/esm/config.js +13 -0
- package/dist/esm/create-themed-styles.d.ts +13 -0
- package/dist/esm/create-themed-styles.js +34 -0
- package/dist/esm/create-themed.d.ts +46 -0
- package/dist/esm/create-themed.js +115 -0
- package/dist/esm/define-theme.d.ts +11 -0
- package/dist/esm/define-theme.js +12 -0
- package/dist/esm/extend-theme.d.ts +14 -0
- package/dist/esm/extend-theme.js +115 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/resolvers/color-resolver.d.ts +10 -0
- package/dist/esm/resolvers/color-resolver.js +23 -0
- package/dist/esm/resolvers/line-height-resolver.d.ts +8 -0
- package/dist/esm/resolvers/line-height-resolver.js +16 -0
- package/dist/esm/resolvers/style-resolver.d.ts +21 -0
- package/dist/esm/resolvers/style-resolver.js +101 -0
- package/dist/esm/semantic-colors.d.ts +14 -0
- package/dist/esm/semantic-colors.js +28 -0
- package/dist/esm/style-props.d.ts +28 -0
- package/dist/esm/style-props.js +92 -0
- package/dist/esm/text-tree.d.ts +30 -0
- package/dist/esm/text-tree.js +72 -0
- package/dist/esm/text-variants.d.ts +22 -0
- package/dist/esm/text-variants.js +26 -0
- package/dist/esm/types.d.ts +281 -0
- package/dist/esm/types.js +1 -0
- package/package.json +74 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 manato
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,86 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @react-native-rethemed/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Type-safe design tokens for React Native style props — no wrapper components. Light/dark aware, with text presets and a `themed` style API. Part of [react-native-rethemed](https://github.com/manakuro/react-native-rethemed).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @react-native-rethemed/core
|
|
9
|
+
npm install --save-dev @react-native-rethemed/cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`react` and `react-native` are peer dependencies.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
Define a theme:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// src/theme/theme.ts
|
|
20
|
+
import { defineTheme } from '@react-native-rethemed/core';
|
|
21
|
+
|
|
22
|
+
export const themeConfig = defineTheme({
|
|
23
|
+
tokens: {
|
|
24
|
+
radii: { md: 6, full: 9999 },
|
|
25
|
+
spacing: { 1: 4, 2: 8, 4: 16 },
|
|
26
|
+
fontSizes: { sm: 14, md: 16 },
|
|
27
|
+
fontWeights: { normal: '400', semibold: '600' },
|
|
28
|
+
lineHeights: { moderate: 1.5 },
|
|
29
|
+
},
|
|
30
|
+
semanticTokens: {
|
|
31
|
+
colors: {
|
|
32
|
+
bg: { default: { light: '#ffffff', dark: '#111111' } },
|
|
33
|
+
fg: { default: { light: '#111111', dark: '#fafafa' } },
|
|
34
|
+
},
|
|
35
|
+
text: {
|
|
36
|
+
title: { fontSize: 'md', lineHeight: 'moderate', fontWeight: 'semibold' },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
defaults: { fontSize: 'md' },
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Generate the typed bindings with [`@react-native-rethemed/cli`](https://github.com/manakuro/react-native-rethemed/tree/main/packages/react-native-rethemed/cli):
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npx @react-native-rethemed/cli codegen src/theme/theme.ts --docs docs/themed.md
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then wrap the app and style with tokens:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { Text, View } from 'react-native';
|
|
53
|
+
import { ThemedProvider, useThemed } from './theme/themed.gen';
|
|
54
|
+
|
|
55
|
+
export default function App() {
|
|
56
|
+
return (
|
|
57
|
+
<ThemedProvider>
|
|
58
|
+
<Card />
|
|
59
|
+
</ThemedProvider>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Card() {
|
|
64
|
+
const { themed } = useThemed();
|
|
65
|
+
return (
|
|
66
|
+
<View style={themed.view({ backgroundColor: 'bg.default', padding: 4, borderRadius: 'md' })}>
|
|
67
|
+
<Text style={themed.text.title({ color: 'fg.default' })}>Hello</Text>
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Start from a design system with a token package (`@react-native-rethemed/chakra-ui-tokens`, `material-ui-tokens`, `material-design-tokens`, `panda-css-tokens`, `tailwind-css-tokens`, `shadcn-ui-tokens`, `radix-ui-tokens`) and `extendTheme`.
|
|
74
|
+
|
|
75
|
+
## Entry points
|
|
76
|
+
|
|
77
|
+
| Import | Contents |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `@react-native-rethemed/core` | Everything: `defineTheme`, `extendTheme`, `createThemed`, `createThemedStyles` and the types |
|
|
80
|
+
| `@react-native-rethemed/core/config` | The React / React Native-free part (`defineTheme`, `extendTheme`, token helpers), for theme files and tooling |
|
|
81
|
+
|
|
82
|
+
See the [main README](https://github.com/manakuro/react-native-rethemed#readme) for color mode, `extendTheme` merge rules and the full API.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type ColorMode = 'light' | 'dark' | 'system';
|
|
2
|
+
/**
|
|
3
|
+
* Structural subset of `expo-secure-store` / `AsyncStorage`, so either can be
|
|
4
|
+
* passed as-is without an adapter.
|
|
5
|
+
*/
|
|
6
|
+
export type ColorModeStorage = {
|
|
7
|
+
getItem: (key: string) => string | null | Promise<string | null>;
|
|
8
|
+
setItem: (key: string, value: string) => void | Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
export declare const isColorMode: (value: unknown) => value is ColorMode;
|
|
11
|
+
/**
|
|
12
|
+
* External store holding the uncontrolled color mode. One is created per
|
|
13
|
+
* `createThemed()` call (never at module level) so independent instances
|
|
14
|
+
* don't share state.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createColorModeStore(): {
|
|
17
|
+
/**
|
|
18
|
+
* Seeds the mode on first use only. Called during render so the very
|
|
19
|
+
* first render already sees `defaultMode`; later calls (e.g. a changed
|
|
20
|
+
* `defaultColorMode` prop) are ignored rather than overwriting the
|
|
21
|
+
* current mode.
|
|
22
|
+
*/
|
|
23
|
+
init(defaultMode: ColorMode): void;
|
|
24
|
+
subscribe(listener: () => void): () => void;
|
|
25
|
+
getSnapshot(): ColorMode;
|
|
26
|
+
setMode(next: ColorMode): void;
|
|
27
|
+
/** Returns `false` if rehydration already ran for this store. */
|
|
28
|
+
beginHydration(): boolean;
|
|
29
|
+
/** Applies a stored value unless it's malformed or the user already chose. */
|
|
30
|
+
hydrate(saved: unknown): void;
|
|
31
|
+
};
|
|
32
|
+
export type ColorModeStore = ReturnType<typeof createColorModeStore>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isColorMode = void 0;
|
|
4
|
+
exports.createColorModeStore = createColorModeStore;
|
|
5
|
+
const isColorMode = (value) => value === 'light' || value === 'dark' || value === 'system';
|
|
6
|
+
exports.isColorMode = isColorMode;
|
|
7
|
+
/**
|
|
8
|
+
* External store holding the uncontrolled color mode. One is created per
|
|
9
|
+
* `createThemed()` call (never at module level) so independent instances
|
|
10
|
+
* don't share state.
|
|
11
|
+
*/
|
|
12
|
+
function createColorModeStore() {
|
|
13
|
+
let mode = null;
|
|
14
|
+
// Set once the user picks a mode, so a late rehydration can't override it.
|
|
15
|
+
let userSet = false;
|
|
16
|
+
let hydrationStarted = false;
|
|
17
|
+
const listeners = new Set();
|
|
18
|
+
const emit = () => {
|
|
19
|
+
for (const listener of listeners)
|
|
20
|
+
listener();
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
/**
|
|
24
|
+
* Seeds the mode on first use only. Called during render so the very
|
|
25
|
+
* first render already sees `defaultMode`; later calls (e.g. a changed
|
|
26
|
+
* `defaultColorMode` prop) are ignored rather than overwriting the
|
|
27
|
+
* current mode.
|
|
28
|
+
*/
|
|
29
|
+
init(defaultMode) {
|
|
30
|
+
if (mode === null)
|
|
31
|
+
mode = defaultMode;
|
|
32
|
+
},
|
|
33
|
+
subscribe(listener) {
|
|
34
|
+
listeners.add(listener);
|
|
35
|
+
return () => {
|
|
36
|
+
listeners.delete(listener);
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
getSnapshot() {
|
|
40
|
+
return mode ?? 'system';
|
|
41
|
+
},
|
|
42
|
+
setMode(next) {
|
|
43
|
+
userSet = true;
|
|
44
|
+
if (next === mode)
|
|
45
|
+
return;
|
|
46
|
+
mode = next;
|
|
47
|
+
emit();
|
|
48
|
+
},
|
|
49
|
+
/** Returns `false` if rehydration already ran for this store. */
|
|
50
|
+
beginHydration() {
|
|
51
|
+
if (hydrationStarted)
|
|
52
|
+
return false;
|
|
53
|
+
hydrationStarted = true;
|
|
54
|
+
return true;
|
|
55
|
+
},
|
|
56
|
+
/** Applies a stored value unless it's malformed or the user already chose. */
|
|
57
|
+
hydrate(saved) {
|
|
58
|
+
if (userSet || !(0, exports.isColorMode)(saved) || saved === mode)
|
|
59
|
+
return;
|
|
60
|
+
mode = saved;
|
|
61
|
+
emit();
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@react-native-rethemed/core/config` — the React/RN-free part of core.
|
|
3
|
+
*
|
|
4
|
+
* Apps can import everything from the package root: the CLI stubs
|
|
5
|
+
* `react-native` when it evaluates a theme file. Core's own token packages
|
|
6
|
+
* still import from here so they never depend on that stub.
|
|
7
|
+
*/
|
|
8
|
+
export { defineTheme } from './define-theme';
|
|
9
|
+
export { extendTheme } from './extend-theme';
|
|
10
|
+
export { RN_DEFAULT_FONT_SIZE, resolveBaseFontSize, } from './resolvers/line-height-resolver';
|
|
11
|
+
export { isSchemeColor, walkSemanticColors } from './semantic-colors';
|
|
12
|
+
export { COLOR_KEYS, FONT_SIZE_KEYS, FONT_WEIGHT_KEYS, LETTER_SPACING_KEYS, LINE_HEIGHT_KEYS, RADIUS_KEYS, SHADOW_STYLE_PROPS, SPACING_KEYS, Z_INDEX_KEYS, } from './style-props';
|
|
13
|
+
export { isTextPreset, resolveTextColor, TEXT_TOKEN_FIELDS, walkTextPresets, } from './text-tree';
|
|
14
|
+
export type { LooseSchema, SchemeColor, SemanticColors, ShadowToken, TextColor, TextToken, TextTokenTree, ThemeConfig, ThemedSchema, ThemedStyles, TokenizeStyle, } from './types';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.walkTextPresets = exports.TEXT_TOKEN_FIELDS = exports.resolveTextColor = exports.isTextPreset = exports.Z_INDEX_KEYS = exports.SPACING_KEYS = exports.SHADOW_STYLE_PROPS = exports.RADIUS_KEYS = exports.LINE_HEIGHT_KEYS = exports.LETTER_SPACING_KEYS = exports.FONT_WEIGHT_KEYS = exports.FONT_SIZE_KEYS = exports.COLOR_KEYS = exports.walkSemanticColors = exports.isSchemeColor = exports.resolveBaseFontSize = exports.RN_DEFAULT_FONT_SIZE = exports.extendTheme = exports.defineTheme = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `@react-native-rethemed/core/config` — the React/RN-free part of core.
|
|
6
|
+
*
|
|
7
|
+
* Apps can import everything from the package root: the CLI stubs
|
|
8
|
+
* `react-native` when it evaluates a theme file. Core's own token packages
|
|
9
|
+
* still import from here so they never depend on that stub.
|
|
10
|
+
*/
|
|
11
|
+
var define_theme_1 = require("./define-theme");
|
|
12
|
+
Object.defineProperty(exports, "defineTheme", { enumerable: true, get: function () { return define_theme_1.defineTheme; } });
|
|
13
|
+
var extend_theme_1 = require("./extend-theme");
|
|
14
|
+
Object.defineProperty(exports, "extendTheme", { enumerable: true, get: function () { return extend_theme_1.extendTheme; } });
|
|
15
|
+
var line_height_resolver_1 = require("./resolvers/line-height-resolver");
|
|
16
|
+
Object.defineProperty(exports, "RN_DEFAULT_FONT_SIZE", { enumerable: true, get: function () { return line_height_resolver_1.RN_DEFAULT_FONT_SIZE; } });
|
|
17
|
+
Object.defineProperty(exports, "resolveBaseFontSize", { enumerable: true, get: function () { return line_height_resolver_1.resolveBaseFontSize; } });
|
|
18
|
+
var semantic_colors_1 = require("./semantic-colors");
|
|
19
|
+
Object.defineProperty(exports, "isSchemeColor", { enumerable: true, get: function () { return semantic_colors_1.isSchemeColor; } });
|
|
20
|
+
Object.defineProperty(exports, "walkSemanticColors", { enumerable: true, get: function () { return semantic_colors_1.walkSemanticColors; } });
|
|
21
|
+
var style_props_1 = require("./style-props");
|
|
22
|
+
Object.defineProperty(exports, "COLOR_KEYS", { enumerable: true, get: function () { return style_props_1.COLOR_KEYS; } });
|
|
23
|
+
Object.defineProperty(exports, "FONT_SIZE_KEYS", { enumerable: true, get: function () { return style_props_1.FONT_SIZE_KEYS; } });
|
|
24
|
+
Object.defineProperty(exports, "FONT_WEIGHT_KEYS", { enumerable: true, get: function () { return style_props_1.FONT_WEIGHT_KEYS; } });
|
|
25
|
+
Object.defineProperty(exports, "LETTER_SPACING_KEYS", { enumerable: true, get: function () { return style_props_1.LETTER_SPACING_KEYS; } });
|
|
26
|
+
Object.defineProperty(exports, "LINE_HEIGHT_KEYS", { enumerable: true, get: function () { return style_props_1.LINE_HEIGHT_KEYS; } });
|
|
27
|
+
Object.defineProperty(exports, "RADIUS_KEYS", { enumerable: true, get: function () { return style_props_1.RADIUS_KEYS; } });
|
|
28
|
+
Object.defineProperty(exports, "SHADOW_STYLE_PROPS", { enumerable: true, get: function () { return style_props_1.SHADOW_STYLE_PROPS; } });
|
|
29
|
+
Object.defineProperty(exports, "SPACING_KEYS", { enumerable: true, get: function () { return style_props_1.SPACING_KEYS; } });
|
|
30
|
+
Object.defineProperty(exports, "Z_INDEX_KEYS", { enumerable: true, get: function () { return style_props_1.Z_INDEX_KEYS; } });
|
|
31
|
+
var text_tree_1 = require("./text-tree");
|
|
32
|
+
Object.defineProperty(exports, "isTextPreset", { enumerable: true, get: function () { return text_tree_1.isTextPreset; } });
|
|
33
|
+
Object.defineProperty(exports, "resolveTextColor", { enumerable: true, get: function () { return text_tree_1.resolveTextColor; } });
|
|
34
|
+
Object.defineProperty(exports, "TEXT_TOKEN_FIELDS", { enumerable: true, get: function () { return text_tree_1.TEXT_TOKEN_FIELDS; } });
|
|
35
|
+
Object.defineProperty(exports, "walkTextPresets", { enumerable: true, get: function () { return text_tree_1.walkTextPresets; } });
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ColorScheme } from './resolvers/color-resolver';
|
|
2
|
+
import type { LooseSchema, ThemeConfig, ThemedSchema, ThemedStyles } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Builds `{ view, text, image }` from a fully resolved `ThemeConfig` for one
|
|
5
|
+
* fixed color scheme. No React here — plain data in, plain functions out.
|
|
6
|
+
* `createThemed` calls this once per scheme and hands the result out through
|
|
7
|
+
* `useThemed()`, so components re-render (and pick up new colors) when the
|
|
8
|
+
* scheme changes.
|
|
9
|
+
*
|
|
10
|
+
* `S` comes from the generated `themed.gen.ts`; without it every token
|
|
11
|
+
* prop is loosely typed (`LooseSchema`).
|
|
12
|
+
*/
|
|
13
|
+
export declare function createThemedStyles<S extends ThemedSchema = LooseSchema>(config: ThemeConfig, scheme: ColorScheme): ThemedStyles<S>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createThemedStyles = createThemedStyles;
|
|
4
|
+
const style_resolver_1 = require("./resolvers/style-resolver");
|
|
5
|
+
const text_tree_1 = require("./text-tree");
|
|
6
|
+
const text_variants_1 = require("./text-variants");
|
|
7
|
+
/**
|
|
8
|
+
* Builds `{ view, text, image }` from a fully resolved `ThemeConfig` for one
|
|
9
|
+
* fixed color scheme. No React here — plain data in, plain functions out.
|
|
10
|
+
* `createThemed` calls this once per scheme and hands the result out through
|
|
11
|
+
* `useThemed()`, so components re-render (and pick up new colors) when the
|
|
12
|
+
* scheme changes.
|
|
13
|
+
*
|
|
14
|
+
* `S` comes from the generated `themed.gen.ts`; without it every token
|
|
15
|
+
* prop is loosely typed (`LooseSchema`).
|
|
16
|
+
*/
|
|
17
|
+
function createThemedStyles(config, scheme) {
|
|
18
|
+
// One pass over the input's keys per call; see `createStyleResolver`.
|
|
19
|
+
const resolveStyle = (0, style_resolver_1.createStyleResolver)(config, scheme);
|
|
20
|
+
const text = (input = {}) => resolveStyle(input);
|
|
21
|
+
// Preset colors (`{ light, dark }`) are picked for this scheme up front.
|
|
22
|
+
const variants = (0, text_variants_1.createTextVariants)((0, text_tree_1.resolveTextTree)(config.semanticTokens?.text, scheme), (input) => resolveStyle(input));
|
|
23
|
+
// `Object.assign` would throw for roles that collide with non-writable
|
|
24
|
+
// function properties (`name`, `length`); defineProperty overrides them.
|
|
25
|
+
for (const [role, sizes] of Object.entries(variants)) {
|
|
26
|
+
Object.defineProperty(text, role, {
|
|
27
|
+
value: sizes,
|
|
28
|
+
enumerable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
view: resolveStyle,
|
|
34
|
+
image: resolveStyle,
|
|
35
|
+
text,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type ColorMode, type ColorModeStorage } from './color-mode-store';
|
|
3
|
+
import { type ColorScheme } from './resolvers/color-resolver';
|
|
4
|
+
import type { LooseSchema, ThemeConfig, ThemedSchema, ThemedStyles } from './types';
|
|
5
|
+
export type { ColorMode, ColorModeStorage, ColorScheme };
|
|
6
|
+
export type ThemedProviderProps = {
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* Controlled mode: the app owns the scheme and this value is used as-is.
|
|
10
|
+
* When set, `defaultColorMode`/`storage`/`storageKey` are ignored.
|
|
11
|
+
*/
|
|
12
|
+
colorScheme?: ColorScheme;
|
|
13
|
+
/** Uncontrolled initial mode. Only read the first time the store is used. */
|
|
14
|
+
defaultColorMode?: ColorMode;
|
|
15
|
+
/** Uncontrolled persistence, e.g. `expo-secure-store` or `AsyncStorage`. */
|
|
16
|
+
storage?: ColorModeStorage;
|
|
17
|
+
storageKey?: string;
|
|
18
|
+
};
|
|
19
|
+
export type UseThemedResult<S extends ThemedSchema = LooseSchema> = {
|
|
20
|
+
themed: ThemedStyles<S>;
|
|
21
|
+
/** Primitive, scheme-independent tokens exactly as in the config. */
|
|
22
|
+
tokens: S['tokens'];
|
|
23
|
+
/**
|
|
24
|
+
* Semantic tokens with colors (including text preset colors) resolved for
|
|
25
|
+
* the current scheme.
|
|
26
|
+
*/
|
|
27
|
+
semanticTokens: S['semanticTokens'];
|
|
28
|
+
colorScheme: ColorScheme;
|
|
29
|
+
};
|
|
30
|
+
export type UseColorModeResult = {
|
|
31
|
+
mode: ColorMode;
|
|
32
|
+
setMode: (mode: ColorMode) => void;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Binds a theme config to React: returns a provider plus hooks that read
|
|
36
|
+
* the current scheme from it. Everything (store, contexts, precomputed
|
|
37
|
+
* themes) is scoped to this call, so several instances can coexist.
|
|
38
|
+
*
|
|
39
|
+
* `S` is supplied by the generated `themed.gen.ts`
|
|
40
|
+
* (`@react-native-rethemed/cli codegen`); without it tokens are loosely typed.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createThemed<S extends ThemedSchema = LooseSchema>(config: ThemeConfig): {
|
|
43
|
+
ThemedProvider: ({ children, colorScheme, defaultColorMode, storage, storageKey, }: ThemedProviderProps) => import("react").FunctionComponentElement<import("react").ProviderProps<ColorScheme | null>>;
|
|
44
|
+
useThemed: () => UseThemedResult<S>;
|
|
45
|
+
useColorMode: () => UseColorModeResult;
|
|
46
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createThemed = createThemed;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
const color_mode_store_1 = require("./color-mode-store");
|
|
7
|
+
const create_themed_styles_1 = require("./create-themed-styles");
|
|
8
|
+
const color_resolver_1 = require("./resolvers/color-resolver");
|
|
9
|
+
const text_tree_1 = require("./text-tree");
|
|
10
|
+
const DEFAULT_STORAGE_KEY = 'react-native-rethemed.color-mode';
|
|
11
|
+
/** RN 0.86 reports `'unspecified'` (and may report `null`); the theme is binary. */
|
|
12
|
+
function toColorScheme(value) {
|
|
13
|
+
return value === 'dark' ? 'dark' : 'light';
|
|
14
|
+
}
|
|
15
|
+
/** `Appearance.setColorScheme` is RN 0.73+ only and absent on react-native-web. */
|
|
16
|
+
function syncNativeAppearance(mode) {
|
|
17
|
+
if (typeof react_native_1.Appearance?.setColorScheme !== 'function')
|
|
18
|
+
return;
|
|
19
|
+
react_native_1.Appearance.setColorScheme(mode === 'system' ? 'unspecified' : mode);
|
|
20
|
+
}
|
|
21
|
+
const warnControlledSetMode = () => {
|
|
22
|
+
if (__DEV__) {
|
|
23
|
+
console.warn('[react-native-rethemed] setMode() was called while <ThemedProvider> is ' +
|
|
24
|
+
'controlled via the `colorScheme` prop. Update that prop instead.');
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Binds a theme config to React: returns a provider plus hooks that read
|
|
29
|
+
* the current scheme from it. Everything (store, contexts, precomputed
|
|
30
|
+
* themes) is scoped to this call, so several instances can coexist.
|
|
31
|
+
*
|
|
32
|
+
* `S` is supplied by the generated `themed.gen.ts`
|
|
33
|
+
* (`@react-native-rethemed/cli codegen`); without it tokens are loosely typed.
|
|
34
|
+
*/
|
|
35
|
+
function createThemed(config) {
|
|
36
|
+
// Precomputed once per scheme so `useThemed()` returns referentially
|
|
37
|
+
// stable objects until the scheme actually changes.
|
|
38
|
+
const build = (colorScheme) => ({
|
|
39
|
+
themed: (0, create_themed_styles_1.createThemedStyles)(config, colorScheme),
|
|
40
|
+
tokens: config.tokens ?? {},
|
|
41
|
+
semanticTokens: {
|
|
42
|
+
colors: (0, color_resolver_1.resolveSemanticColors)(config, colorScheme),
|
|
43
|
+
text: (0, text_tree_1.resolveTextTree)(config.semanticTokens?.text, colorScheme),
|
|
44
|
+
},
|
|
45
|
+
colorScheme,
|
|
46
|
+
});
|
|
47
|
+
const themes = {
|
|
48
|
+
light: build('light'),
|
|
49
|
+
dark: build('dark'),
|
|
50
|
+
};
|
|
51
|
+
const store = (0, color_mode_store_1.createColorModeStore)();
|
|
52
|
+
// Split so a mode change that keeps the same scheme (e.g. 'system' ->
|
|
53
|
+
// 'light' while the OS is light) doesn't re-render `useThemed` consumers.
|
|
54
|
+
const SchemeContext = (0, react_1.createContext)(null);
|
|
55
|
+
const ModeContext = (0, react_1.createContext)(null);
|
|
56
|
+
function ThemedProvider({ children, colorScheme, defaultColorMode = 'system', storage, storageKey = DEFAULT_STORAGE_KEY, }) {
|
|
57
|
+
const isControlled = colorScheme !== undefined;
|
|
58
|
+
// Hooks run unconditionally so switching between controlled and
|
|
59
|
+
// uncontrolled never changes hook order or remounts children.
|
|
60
|
+
store.init(defaultColorMode);
|
|
61
|
+
const storedMode = (0, react_1.useSyncExternalStore)(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
62
|
+
const systemScheme = toColorScheme((0, react_native_1.useColorScheme)());
|
|
63
|
+
(0, react_1.useEffect)(() => {
|
|
64
|
+
if (isControlled || !storage || !store.beginHydration())
|
|
65
|
+
return;
|
|
66
|
+
// The store outlives this provider, so the result is applied even if
|
|
67
|
+
// it resolves after unmount; `hydrate` drops it if the user already
|
|
68
|
+
// picked a mode. `Promise.resolve().then` also catches a sync throw.
|
|
69
|
+
Promise.resolve()
|
|
70
|
+
.then(() => storage.getItem(storageKey))
|
|
71
|
+
.then((saved) => store.hydrate(saved))
|
|
72
|
+
.catch(() => { });
|
|
73
|
+
}, [isControlled, storage, storageKey]);
|
|
74
|
+
(0, react_1.useEffect)(() => {
|
|
75
|
+
if (!isControlled)
|
|
76
|
+
syncNativeAppearance(storedMode);
|
|
77
|
+
}, [isControlled, storedMode]);
|
|
78
|
+
const setUncontrolledMode = (0, react_1.useCallback)((next) => {
|
|
79
|
+
// Sync first: when returning to 'system', RN's `useColorScheme()`
|
|
80
|
+
// keeps reporting the old override until it's reset.
|
|
81
|
+
syncNativeAppearance(next);
|
|
82
|
+
store.setMode(next);
|
|
83
|
+
if (storage) {
|
|
84
|
+
Promise.resolve()
|
|
85
|
+
.then(() => storage.setItem(storageKey, next))
|
|
86
|
+
.catch(() => { });
|
|
87
|
+
}
|
|
88
|
+
}, [storage, storageKey]);
|
|
89
|
+
const scheme = isControlled
|
|
90
|
+
? colorScheme
|
|
91
|
+
: storedMode === 'system'
|
|
92
|
+
? systemScheme
|
|
93
|
+
: storedMode;
|
|
94
|
+
const mode = isControlled ? colorScheme : storedMode;
|
|
95
|
+
const setMode = isControlled ? warnControlledSetMode : setUncontrolledMode;
|
|
96
|
+
const modeValue = (0, react_1.useMemo)(() => ({ mode, setMode }), [mode, setMode]);
|
|
97
|
+
// `createElement` instead of JSX so consumers type-checking this source
|
|
98
|
+
// (it ships as `.ts`) don't need a `jsx` compiler option.
|
|
99
|
+
return (0, react_1.createElement)(SchemeContext, { value: scheme }, (0, react_1.createElement)(ModeContext, { value: modeValue }, children));
|
|
100
|
+
}
|
|
101
|
+
function useThemed() {
|
|
102
|
+
const scheme = (0, react_1.useContext)(SchemeContext);
|
|
103
|
+
if (scheme === null) {
|
|
104
|
+
throw new Error('useThemed() must be used within a <ThemedProvider> returned by the ' +
|
|
105
|
+
'same createThemed() call.');
|
|
106
|
+
}
|
|
107
|
+
return themes[scheme];
|
|
108
|
+
}
|
|
109
|
+
function useColorMode() {
|
|
110
|
+
const value = (0, react_1.useContext)(ModeContext);
|
|
111
|
+
if (value === null) {
|
|
112
|
+
throw new Error('useColorMode() must be used within a <ThemedProvider> returned by ' +
|
|
113
|
+
'the same createThemed() call.');
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
return { ThemedProvider, useThemed, useColorMode };
|
|
118
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ThemeConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Identity function used purely as a type gate: checks `config` against
|
|
4
|
+
* `ThemeConfig` (object literals get the usual excess-property check, so a
|
|
5
|
+
* misspelled `radius` or `semanticToken` fails to compile).
|
|
6
|
+
*
|
|
7
|
+
* The return type is deliberately the plain `ThemeConfig` — exact token
|
|
8
|
+
* names come from the generated `themed.gen.ts`, not from literal-type
|
|
9
|
+
* inference here.
|
|
10
|
+
*/
|
|
11
|
+
export declare function defineTheme(config: ThemeConfig): ThemeConfig;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defineTheme = defineTheme;
|
|
4
|
+
/**
|
|
5
|
+
* Identity function used purely as a type gate: checks `config` against
|
|
6
|
+
* `ThemeConfig` (object literals get the usual excess-property check, so a
|
|
7
|
+
* misspelled `radius` or `semanticToken` fails to compile).
|
|
8
|
+
*
|
|
9
|
+
* The return type is deliberately the plain `ThemeConfig` — exact token
|
|
10
|
+
* names come from the generated `themed.gen.ts`, not from literal-type
|
|
11
|
+
* inference here.
|
|
12
|
+
*/
|
|
13
|
+
function defineTheme(config) {
|
|
14
|
+
return config;
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ThemeConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Merges N `ThemeConfig`s, folding left-to-right so later themes override
|
|
4
|
+
* earlier ones per key:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const config = extendTheme(materialDesignTheme, chakraUiTheme, {
|
|
8
|
+
* semanticTokens: {
|
|
9
|
+
* text: { display: { lg: { fontSize: 60, lineHeight: 68 } } },
|
|
10
|
+
* },
|
|
11
|
+
* });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare function extendTheme(...themes: [ThemeConfig, ...ThemeConfig[]]): ThemeConfig;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extendTheme = extendTheme;
|
|
4
|
+
const semantic_colors_1 = require("./semantic-colors");
|
|
5
|
+
const text_tree_1 = require("./text-tree");
|
|
6
|
+
/**
|
|
7
|
+
* Flat `tokens` categories: a one-level merge (`{ ...a, ...b }`, `b` wins per
|
|
8
|
+
* key) is enough. `semanticTokens.colors` holds groups (merged one level
|
|
9
|
+
* deeper) and top-level colors (replaced whole); `semanticTokens.text` is a tree of any depth — see
|
|
10
|
+
* `mergeTextTree`.
|
|
11
|
+
*
|
|
12
|
+
* Merging is runtime-only: the result is typed as the plain `ThemeConfig`,
|
|
13
|
+
* and exact token names come from the generated `themed.gen.ts`.
|
|
14
|
+
*/
|
|
15
|
+
const FLAT_CATEGORY_KEYS = [
|
|
16
|
+
'colors',
|
|
17
|
+
'radii',
|
|
18
|
+
'spacing',
|
|
19
|
+
'fontSizes',
|
|
20
|
+
'fontWeights',
|
|
21
|
+
'lineHeights',
|
|
22
|
+
'letterSpacings',
|
|
23
|
+
'zIndices',
|
|
24
|
+
'shadows',
|
|
25
|
+
];
|
|
26
|
+
function mergeFlat(a, b) {
|
|
27
|
+
if (!a)
|
|
28
|
+
return b;
|
|
29
|
+
if (!b)
|
|
30
|
+
return a;
|
|
31
|
+
return { ...a, ...b };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* `semanticTokens.colors`: groups are merged, colors replaced whole, so
|
|
35
|
+
* overriding `bg.default` keeps `bg.subtle` and every other group. A
|
|
36
|
+
* top-level color (`primary`) is replaced like a token; when a name is a
|
|
37
|
+
* color on one side and a group on the other, the later side replaces it.
|
|
38
|
+
*/
|
|
39
|
+
function mergeSemanticColors(a, b) {
|
|
40
|
+
if (!a)
|
|
41
|
+
return b;
|
|
42
|
+
if (!b)
|
|
43
|
+
return a;
|
|
44
|
+
const result = { ...a };
|
|
45
|
+
for (const [key, next] of Object.entries(b)) {
|
|
46
|
+
const prev = result[key];
|
|
47
|
+
result[key] =
|
|
48
|
+
prev && !(0, semantic_colors_1.isSchemeColor)(prev) && !(0, semantic_colors_1.isSchemeColor)(next)
|
|
49
|
+
? { ...prev, ...next }
|
|
50
|
+
: next;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* `semanticTokens.text`: groups merge recursively, presets (leaves) are
|
|
56
|
+
* replaced whole. So overriding `display.lg` keeps `display.md` and every
|
|
57
|
+
* other group, at any depth. When a key is a preset on one side and a group
|
|
58
|
+
* on the other, the later side replaces it.
|
|
59
|
+
*/
|
|
60
|
+
function mergeTextTree(a, b) {
|
|
61
|
+
if (!a)
|
|
62
|
+
return b;
|
|
63
|
+
if (!b)
|
|
64
|
+
return a;
|
|
65
|
+
const result = { ...a };
|
|
66
|
+
for (const [key, next] of Object.entries(b)) {
|
|
67
|
+
const prev = result[key];
|
|
68
|
+
result[key] =
|
|
69
|
+
prev && !(0, text_tree_1.isTextPreset)(prev) && !(0, text_tree_1.isTextPreset)(next)
|
|
70
|
+
? (mergeTextTree(prev, next) ?? next)
|
|
71
|
+
: next;
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
function mergeTokens(a, b) {
|
|
76
|
+
if (!a)
|
|
77
|
+
return b;
|
|
78
|
+
if (!b)
|
|
79
|
+
return a;
|
|
80
|
+
const merged = {};
|
|
81
|
+
for (const key of FLAT_CATEGORY_KEYS) {
|
|
82
|
+
const value = mergeFlat(a[key], b[key]);
|
|
83
|
+
if (value)
|
|
84
|
+
merged[key] = value;
|
|
85
|
+
}
|
|
86
|
+
return merged;
|
|
87
|
+
}
|
|
88
|
+
function mergeSemanticTokens(a, b) {
|
|
89
|
+
const colors = mergeSemanticColors(a?.colors, b?.colors);
|
|
90
|
+
const text = mergeTextTree(a?.text, b?.text);
|
|
91
|
+
return {
|
|
92
|
+
...(colors ? { colors } : {}),
|
|
93
|
+
...(text ? { text } : {}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function mergeThemeConfig(a, b) {
|
|
97
|
+
const defaults = mergeFlat(a.defaults, b.defaults);
|
|
98
|
+
return {
|
|
99
|
+
tokens: mergeTokens(a.tokens, b.tokens),
|
|
100
|
+
semanticTokens: mergeSemanticTokens(a.semanticTokens, b.semanticTokens),
|
|
101
|
+
...(defaults ? { defaults } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Merges N `ThemeConfig`s, folding left-to-right so later themes override
|
|
106
|
+
* earlier ones per key:
|
|
107
|
+
*
|
|
108
|
+
* ```ts
|
|
109
|
+
* const config = extendTheme(materialDesignTheme, chakraUiTheme, {
|
|
110
|
+
* semanticTokens: {
|
|
111
|
+
* text: { display: { lg: { fontSize: 60, lineHeight: 68 } } },
|
|
112
|
+
* },
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
function extendTheme(...themes) {
|
|
117
|
+
return themes.reduce(mergeThemeConfig);
|
|
118
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from './config';
|
|
2
|
+
export type { ColorMode, ColorModeStorage, ColorScheme, ThemedProviderProps, UseColorModeResult, UseThemedResult, } from './create-themed';
|
|
3
|
+
export { createThemed } from './create-themed';
|
|
4
|
+
export { createThemedStyles } from './create-themed-styles';
|