@native-mate/core 0.1.0 → 0.1.1
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/README.md +102 -0
- package/dist/primitives/Spinner/Spinner.js +2 -2
- package/dist/primitives/Text/Text.js +2 -2
- package/package.json +31 -31
- package/src/__tests__/makeStyles.test.ts +56 -56
- package/src/__tests__/perf.test.ts +46 -46
- package/src/__tests__/platform.test.ts +34 -34
- package/src/__tests__/theme.test.ts +58 -58
- package/src/__tests__/tokens.test.ts +105 -105
- package/src/index.ts +27 -27
- package/src/primitives/Icon/Icon.tsx +11 -11
- package/src/primitives/Icon/Icon.types.ts +7 -7
- package/src/primitives/Separator/Separator.tsx +22 -22
- package/src/primitives/Separator/Separator.types.ts +6 -6
- package/src/primitives/Spinner/Spinner.tsx +50 -50
- package/src/primitives/Spinner/Spinner.types.ts +4 -4
- package/src/primitives/Text/Text.tsx +45 -45
- package/src/primitives/Text/Text.types.ts +15 -15
- package/src/theme/ThemeContext.ts +6 -6
- package/src/theme/ThemeProvider.tsx +27 -27
- package/src/theme/makeStyles.ts +15 -15
- package/src/theme/useTheme.ts +7 -7
- package/src/tokens/index.ts +29 -29
- package/src/tokens/presets/midnight.ts +24 -24
- package/src/tokens/presets/rose.ts +24 -24
- package/src/tokens/presets/slate.ts +24 -24
- package/src/tokens/presets/zinc.ts +37 -37
- package/src/tokens/types.ts +72 -72
- package/src/utils/platform.ts +20 -20
- package/src/utils/useBreakpoint.ts +10 -10
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @native-mate/core
|
|
2
|
+
|
|
3
|
+
Token system, theme provider, and primitive components for [native-mate](https://github.com/native-mate/native-mate).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @native-mate/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Wrap your app in `ThemeProvider`:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { ThemeProvider } from '@native-mate/core'
|
|
17
|
+
|
|
18
|
+
export default function App() {
|
|
19
|
+
return (
|
|
20
|
+
<ThemeProvider preset="zinc">
|
|
21
|
+
<YourApp />
|
|
22
|
+
</ThemeProvider>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Presets
|
|
28
|
+
|
|
29
|
+
Four built-in presets: `zinc`, `slate`, `rose`, `midnight`
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
<ThemeProvider preset="zinc" /> // neutral grays (default)
|
|
33
|
+
<ThemeProvider preset="slate" /> // cool blue-gray
|
|
34
|
+
<ThemeProvider preset="rose" /> // warm rose accents
|
|
35
|
+
<ThemeProvider preset="midnight" /> // deep dark with purple
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Hooks
|
|
39
|
+
|
|
40
|
+
### `useTheme()`
|
|
41
|
+
|
|
42
|
+
Access resolved tokens inside any component:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { useTheme } from '@native-mate/core'
|
|
46
|
+
|
|
47
|
+
function MyComponent() {
|
|
48
|
+
const theme = useTheme()
|
|
49
|
+
return <View style={{ backgroundColor: theme.colors.background }} />
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `makeStyles(factory)`
|
|
54
|
+
|
|
55
|
+
Create memoized, theme-aware StyleSheets:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { makeStyles } from '@native-mate/core'
|
|
59
|
+
|
|
60
|
+
const useStyles = makeStyles((theme) => ({
|
|
61
|
+
container: {
|
|
62
|
+
backgroundColor: theme.colors.surface,
|
|
63
|
+
padding: theme.spacing.lg,
|
|
64
|
+
borderRadius: theme.radius.md,
|
|
65
|
+
},
|
|
66
|
+
}))
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### `useBreakpoint()`
|
|
70
|
+
|
|
71
|
+
Responsive breakpoints: `sm` (<768), `md` (768–1023), `lg` (1024+)
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { useBreakpoint } from '@native-mate/core'
|
|
75
|
+
|
|
76
|
+
const bp = useBreakpoint() // 'sm' | 'md' | 'lg'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Token Categories
|
|
80
|
+
|
|
81
|
+
| Category | Examples |
|
|
82
|
+
|---|---|
|
|
83
|
+
| Colors | `background`, `foreground`, `primary`, `destructive`, `muted` |
|
|
84
|
+
| Spacing | `xs` (4), `sm` (8), `md` (12), `lg` (16), `xl` (24) |
|
|
85
|
+
| Radius | `sm` (6), `md` (10), `lg` (16), `full` (9999) |
|
|
86
|
+
| Typography | `size`, `weight`, `lineHeight` |
|
|
87
|
+
| Animation | `speed.fast` (150ms), `speed.normal` (250ms) |
|
|
88
|
+
|
|
89
|
+
## Overrides
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<ThemeProvider
|
|
93
|
+
preset="zinc"
|
|
94
|
+
overrides={{
|
|
95
|
+
dark: { colors: { primary: '#6366f1' } },
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
@@ -14,7 +14,7 @@ export const Spinner = ({ size = 'md', color }) => {
|
|
|
14
14
|
const animatedStyle = useAnimatedStyle(() => ({
|
|
15
15
|
transform: [{ rotate: `${rotation.value}deg` }],
|
|
16
16
|
}));
|
|
17
|
-
return (<View style={{ width: px, height: px }}>
|
|
17
|
+
return (<View style={{ width: px, height: px }}>
|
|
18
18
|
<Animated.View style={[
|
|
19
19
|
animatedStyle,
|
|
20
20
|
{
|
|
@@ -25,7 +25,7 @@ export const Spinner = ({ size = 'md', color }) => {
|
|
|
25
25
|
borderColor: spinnerColor,
|
|
26
26
|
borderTopColor: 'transparent',
|
|
27
27
|
},
|
|
28
|
-
]}/>
|
|
28
|
+
]}/>
|
|
29
29
|
</View>);
|
|
30
30
|
};
|
|
31
31
|
//# sourceMappingURL=Spinner.js.map
|
|
@@ -20,8 +20,8 @@ export const Text = ({ variant = 'body', size, weight, color, muted = false, sty
|
|
|
20
20
|
lineHeight: theme.typography.lineHeight.normal,
|
|
21
21
|
},
|
|
22
22
|
style,
|
|
23
|
-
]} {...rest}>
|
|
24
|
-
{children}
|
|
23
|
+
]} {...rest}>
|
|
24
|
+
{children}
|
|
25
25
|
</RNText>);
|
|
26
26
|
};
|
|
27
27
|
//# sourceMappingURL=Text.js.map
|
package/package.json
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@native-mate/core",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Token system, ThemeProvider, and primitive components for native-mate",
|
|
5
|
-
"main": "./dist/index.js",
|
|
6
|
-
"types": "./dist/index.d.ts",
|
|
7
|
-
"files": ["dist", "src"],
|
|
8
|
-
"license": "MIT",
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "https://github.com/native-mate/native-mate",
|
|
12
|
-
"directory": "packages/core"
|
|
13
|
-
},
|
|
14
|
-
"keywords": ["react-native", "expo", "ui", "theme", "tokens", "native-mate"],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "tsc --project tsconfig.json",
|
|
17
|
-
"test": "cd ../.. && npx vitest run --config vitest.config.ts packages/core",
|
|
18
|
-
"lint": "tsc --noEmit"
|
|
19
|
-
},
|
|
20
|
-
"peerDependencies": {
|
|
21
|
-
"react": ">=18.0.0",
|
|
22
|
-
"react-native": ">=0.73.0",
|
|
23
|
-
"react-native-reanimated": ">=3.0.0"
|
|
24
|
-
},
|
|
25
|
-
"devDependencies": {
|
|
26
|
-
"react": "18.2.0",
|
|
27
|
-
"react-native": "0.73.6",
|
|
28
|
-
"react-native-reanimated": "^3.6.0",
|
|
29
|
-
"typescript": "^5.4.0"
|
|
30
|
-
}
|
|
31
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@native-mate/core",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Token system, ThemeProvider, and primitive components for native-mate",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"files": ["dist", "src"],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/native-mate/native-mate",
|
|
12
|
+
"directory": "packages/core"
|
|
13
|
+
},
|
|
14
|
+
"keywords": ["react-native", "expo", "ui", "theme", "tokens", "native-mate"],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc --project tsconfig.json",
|
|
17
|
+
"test": "cd ../.. && npx vitest run --config vitest.config.ts packages/core",
|
|
18
|
+
"lint": "tsc --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"react": ">=18.0.0",
|
|
22
|
+
"react-native": ">=0.73.0",
|
|
23
|
+
"react-native-reanimated": ">=3.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"react": "18.2.0",
|
|
27
|
+
"react-native": "0.73.6",
|
|
28
|
+
"react-native-reanimated": "^3.6.0",
|
|
29
|
+
"typescript": "^5.4.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { resolveTokens, zinc } from '../tokens'
|
|
3
|
-
import { makeStyles } from '../theme/makeStyles'
|
|
4
|
-
|
|
5
|
-
// makeStyles is a factory that returns a hook. We can't call hooks outside
|
|
6
|
-
// React, but we CAN verify the factory itself and the styles it produces.
|
|
7
|
-
|
|
8
|
-
describe('makeStyles', () => {
|
|
9
|
-
it('returns a function (hook)', () => {
|
|
10
|
-
const useStyles = makeStyles((theme) => ({
|
|
11
|
-
container: { backgroundColor: theme.colors.background },
|
|
12
|
-
}))
|
|
13
|
-
expect(typeof useStyles).toBe('function')
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('factory receives a theme and returns style objects', () => {
|
|
17
|
-
const theme = resolveTokens(zinc, 'dark')
|
|
18
|
-
const factory = (t: typeof theme) => ({
|
|
19
|
-
box: { padding: t.spacing.md, borderRadius: t.radius.md },
|
|
20
|
-
})
|
|
21
|
-
const styles = factory(theme)
|
|
22
|
-
expect(styles.box.padding).toBe(12)
|
|
23
|
-
expect(styles.box.borderRadius).toBe(10)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('factory can access all token categories', () => {
|
|
27
|
-
const theme = resolveTokens(zinc, 'dark')
|
|
28
|
-
const factory = (t: typeof theme) => ({
|
|
29
|
-
text: {
|
|
30
|
-
color: t.colors.foreground,
|
|
31
|
-
fontSize: t.typography.size.md,
|
|
32
|
-
fontWeight: t.typography.weight.bold,
|
|
33
|
-
lineHeight: t.typography.lineHeight.normal,
|
|
34
|
-
},
|
|
35
|
-
animated: {
|
|
36
|
-
// animation tokens are accessible
|
|
37
|
-
opacity: t.animation.speed.fast > 0 ? 1 : 0,
|
|
38
|
-
},
|
|
39
|
-
})
|
|
40
|
-
const styles = factory(theme)
|
|
41
|
-
expect(styles.text.color).toBe('#fafafa')
|
|
42
|
-
expect(styles.text.fontSize).toBe(15)
|
|
43
|
-
expect(styles.text.fontWeight).toBe('700')
|
|
44
|
-
expect(styles.animated.opacity).toBe(1)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('produces different styles for different presets', () => {
|
|
48
|
-
const darkTheme = resolveTokens(zinc, 'dark')
|
|
49
|
-
const lightTheme = resolveTokens(zinc, 'light')
|
|
50
|
-
const factory = (t: typeof darkTheme) => ({
|
|
51
|
-
bg: { backgroundColor: t.colors.background },
|
|
52
|
-
})
|
|
53
|
-
expect(factory(darkTheme).bg.backgroundColor).toBe('#070709')
|
|
54
|
-
expect(factory(lightTheme).bg.backgroundColor).toBe('#ffffff')
|
|
55
|
-
})
|
|
56
|
-
})
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { resolveTokens, zinc } from '../tokens'
|
|
3
|
+
import { makeStyles } from '../theme/makeStyles'
|
|
4
|
+
|
|
5
|
+
// makeStyles is a factory that returns a hook. We can't call hooks outside
|
|
6
|
+
// React, but we CAN verify the factory itself and the styles it produces.
|
|
7
|
+
|
|
8
|
+
describe('makeStyles', () => {
|
|
9
|
+
it('returns a function (hook)', () => {
|
|
10
|
+
const useStyles = makeStyles((theme) => ({
|
|
11
|
+
container: { backgroundColor: theme.colors.background },
|
|
12
|
+
}))
|
|
13
|
+
expect(typeof useStyles).toBe('function')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('factory receives a theme and returns style objects', () => {
|
|
17
|
+
const theme = resolveTokens(zinc, 'dark')
|
|
18
|
+
const factory = (t: typeof theme) => ({
|
|
19
|
+
box: { padding: t.spacing.md, borderRadius: t.radius.md },
|
|
20
|
+
})
|
|
21
|
+
const styles = factory(theme)
|
|
22
|
+
expect(styles.box.padding).toBe(12)
|
|
23
|
+
expect(styles.box.borderRadius).toBe(10)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('factory can access all token categories', () => {
|
|
27
|
+
const theme = resolveTokens(zinc, 'dark')
|
|
28
|
+
const factory = (t: typeof theme) => ({
|
|
29
|
+
text: {
|
|
30
|
+
color: t.colors.foreground,
|
|
31
|
+
fontSize: t.typography.size.md,
|
|
32
|
+
fontWeight: t.typography.weight.bold,
|
|
33
|
+
lineHeight: t.typography.lineHeight.normal,
|
|
34
|
+
},
|
|
35
|
+
animated: {
|
|
36
|
+
// animation tokens are accessible
|
|
37
|
+
opacity: t.animation.speed.fast > 0 ? 1 : 0,
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
const styles = factory(theme)
|
|
41
|
+
expect(styles.text.color).toBe('#fafafa')
|
|
42
|
+
expect(styles.text.fontSize).toBe(15)
|
|
43
|
+
expect(styles.text.fontWeight).toBe('700')
|
|
44
|
+
expect(styles.animated.opacity).toBe(1)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('produces different styles for different presets', () => {
|
|
48
|
+
const darkTheme = resolveTokens(zinc, 'dark')
|
|
49
|
+
const lightTheme = resolveTokens(zinc, 'light')
|
|
50
|
+
const factory = (t: typeof darkTheme) => ({
|
|
51
|
+
bg: { backgroundColor: t.colors.background },
|
|
52
|
+
})
|
|
53
|
+
expect(factory(darkTheme).bg.backgroundColor).toBe('#070709')
|
|
54
|
+
expect(factory(lightTheme).bg.backgroundColor).toBe('#ffffff')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { resolveTokens, zinc, slate, rose, midnight, presets } from '../tokens'
|
|
3
|
-
|
|
4
|
-
describe('performance', () => {
|
|
5
|
-
it('resolveTokens runs 10,000 times under 200ms', () => {
|
|
6
|
-
const start = performance.now()
|
|
7
|
-
for (let i = 0; i < 10_000; i++) {
|
|
8
|
-
resolveTokens(zinc, i % 2 === 0 ? 'dark' : 'light')
|
|
9
|
-
}
|
|
10
|
-
const elapsed = performance.now() - start
|
|
11
|
-
expect(elapsed).toBeLessThan(200)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('resolveTokens with overrides runs 10,000 times under 300ms', () => {
|
|
15
|
-
const overrides = { colors: { primary: '#6366f1' }, spacing: { lg: 20 } }
|
|
16
|
-
const start = performance.now()
|
|
17
|
-
for (let i = 0; i < 10_000; i++) {
|
|
18
|
-
resolveTokens(zinc, 'dark', overrides)
|
|
19
|
-
}
|
|
20
|
-
const elapsed = performance.now() - start
|
|
21
|
-
expect(elapsed).toBeLessThan(300)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('all 4 presets × 2 modes (80,000 calls) under 1s', () => {
|
|
25
|
-
const allPresets = [zinc, slate, rose, midnight]
|
|
26
|
-
const start = performance.now()
|
|
27
|
-
for (let i = 0; i < 10_000; i++) {
|
|
28
|
-
for (const preset of allPresets) {
|
|
29
|
-
resolveTokens(preset, 'dark')
|
|
30
|
-
resolveTokens(preset, 'light')
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
const elapsed = performance.now() - start
|
|
34
|
-
expect(elapsed).toBeLessThan(1000)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('resolved theme object is consistently shaped', () => {
|
|
38
|
-
const theme = resolveTokens(zinc, 'dark')
|
|
39
|
-
const colorKeys = Object.keys(theme.colors)
|
|
40
|
-
// Verify across all presets
|
|
41
|
-
for (const preset of Object.values(presets)) {
|
|
42
|
-
const t = resolveTokens(preset, 'dark')
|
|
43
|
-
expect(Object.keys(t.colors).sort()).toEqual(colorKeys.sort())
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
|
-
})
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { resolveTokens, zinc, slate, rose, midnight, presets } from '../tokens'
|
|
3
|
+
|
|
4
|
+
describe('performance', () => {
|
|
5
|
+
it('resolveTokens runs 10,000 times under 200ms', () => {
|
|
6
|
+
const start = performance.now()
|
|
7
|
+
for (let i = 0; i < 10_000; i++) {
|
|
8
|
+
resolveTokens(zinc, i % 2 === 0 ? 'dark' : 'light')
|
|
9
|
+
}
|
|
10
|
+
const elapsed = performance.now() - start
|
|
11
|
+
expect(elapsed).toBeLessThan(200)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('resolveTokens with overrides runs 10,000 times under 300ms', () => {
|
|
15
|
+
const overrides = { colors: { primary: '#6366f1' }, spacing: { lg: 20 } }
|
|
16
|
+
const start = performance.now()
|
|
17
|
+
for (let i = 0; i < 10_000; i++) {
|
|
18
|
+
resolveTokens(zinc, 'dark', overrides)
|
|
19
|
+
}
|
|
20
|
+
const elapsed = performance.now() - start
|
|
21
|
+
expect(elapsed).toBeLessThan(300)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('all 4 presets × 2 modes (80,000 calls) under 1s', () => {
|
|
25
|
+
const allPresets = [zinc, slate, rose, midnight]
|
|
26
|
+
const start = performance.now()
|
|
27
|
+
for (let i = 0; i < 10_000; i++) {
|
|
28
|
+
for (const preset of allPresets) {
|
|
29
|
+
resolveTokens(preset, 'dark')
|
|
30
|
+
resolveTokens(preset, 'light')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const elapsed = performance.now() - start
|
|
34
|
+
expect(elapsed).toBeLessThan(1000)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('resolved theme object is consistently shaped', () => {
|
|
38
|
+
const theme = resolveTokens(zinc, 'dark')
|
|
39
|
+
const colorKeys = Object.keys(theme.colors)
|
|
40
|
+
// Verify across all presets
|
|
41
|
+
for (const preset of Object.values(presets)) {
|
|
42
|
+
const t = resolveTokens(preset, 'dark')
|
|
43
|
+
expect(Object.keys(t.colors).sort()).toEqual(colorKeys.sort())
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { Platform } from 'react-native'
|
|
3
|
-
import { shadow } from '../utils/platform'
|
|
4
|
-
|
|
5
|
-
describe('shadow()', () => {
|
|
6
|
-
it('returns elevation on Android', () => {
|
|
7
|
-
Object.defineProperty(Platform, 'OS', { get: () => 'android', configurable: true })
|
|
8
|
-
const s = shadow(2)
|
|
9
|
-
expect(s).toHaveProperty('elevation')
|
|
10
|
-
expect((s as { elevation: number }).elevation).toBe(4)
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('returns shadowColor and shadowOffset on iOS', () => {
|
|
14
|
-
Object.defineProperty(Platform, 'OS', { get: () => 'ios', configurable: true })
|
|
15
|
-
const s = shadow(1)
|
|
16
|
-
expect(s).toHaveProperty('shadowColor')
|
|
17
|
-
expect(s).toHaveProperty('shadowOffset')
|
|
18
|
-
expect(s).toHaveProperty('shadowOpacity')
|
|
19
|
-
expect(s).toHaveProperty('shadowRadius')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('level 1 has lower shadow than level 4', () => {
|
|
23
|
-
Object.defineProperty(Platform, 'OS', { get: () => 'android', configurable: true })
|
|
24
|
-
const s1 = shadow(1) as { elevation: number }
|
|
25
|
-
const s4 = shadow(4) as { elevation: number }
|
|
26
|
-
expect(s4.elevation).toBeGreaterThan(s1.elevation)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('returns elevation object on non-ios (android fallback)', () => {
|
|
30
|
-
Object.defineProperty(Platform, 'OS', { get: () => 'android', configurable: true })
|
|
31
|
-
const s = shadow(3) as { elevation: number }
|
|
32
|
-
expect(s.elevation).toBe(8)
|
|
33
|
-
})
|
|
34
|
-
})
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { Platform } from 'react-native'
|
|
3
|
+
import { shadow } from '../utils/platform'
|
|
4
|
+
|
|
5
|
+
describe('shadow()', () => {
|
|
6
|
+
it('returns elevation on Android', () => {
|
|
7
|
+
Object.defineProperty(Platform, 'OS', { get: () => 'android', configurable: true })
|
|
8
|
+
const s = shadow(2)
|
|
9
|
+
expect(s).toHaveProperty('elevation')
|
|
10
|
+
expect((s as { elevation: number }).elevation).toBe(4)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('returns shadowColor and shadowOffset on iOS', () => {
|
|
14
|
+
Object.defineProperty(Platform, 'OS', { get: () => 'ios', configurable: true })
|
|
15
|
+
const s = shadow(1)
|
|
16
|
+
expect(s).toHaveProperty('shadowColor')
|
|
17
|
+
expect(s).toHaveProperty('shadowOffset')
|
|
18
|
+
expect(s).toHaveProperty('shadowOpacity')
|
|
19
|
+
expect(s).toHaveProperty('shadowRadius')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('level 1 has lower shadow than level 4', () => {
|
|
23
|
+
Object.defineProperty(Platform, 'OS', { get: () => 'android', configurable: true })
|
|
24
|
+
const s1 = shadow(1) as { elevation: number }
|
|
25
|
+
const s4 = shadow(4) as { elevation: number }
|
|
26
|
+
expect(s4.elevation).toBeGreaterThan(s1.elevation)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns elevation object on non-ios (android fallback)', () => {
|
|
30
|
+
Object.defineProperty(Platform, 'OS', { get: () => 'android', configurable: true })
|
|
31
|
+
const s = shadow(3) as { elevation: number }
|
|
32
|
+
expect(s.elevation).toBe(8)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { defaultTheme, ThemeContext } from '../theme/ThemeContext'
|
|
3
|
-
import { resolveTokens, zinc, presets } from '../tokens'
|
|
4
|
-
|
|
5
|
-
describe('ThemeContext', () => {
|
|
6
|
-
it('defaultTheme is zinc light', () => {
|
|
7
|
-
const expected = resolveTokens(zinc, 'light')
|
|
8
|
-
expect(defaultTheme.colorScheme).toBe('light')
|
|
9
|
-
expect(defaultTheme.colors.background).toBe(expected.colors.background)
|
|
10
|
-
expect(defaultTheme.colors.foreground).toBe(expected.colors.foreground)
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('defaultTheme has all required token categories', () => {
|
|
14
|
-
expect(defaultTheme).toHaveProperty('colors')
|
|
15
|
-
expect(defaultTheme).toHaveProperty('spacing')
|
|
16
|
-
expect(defaultTheme).toHaveProperty('radius')
|
|
17
|
-
expect(defaultTheme).toHaveProperty('typography')
|
|
18
|
-
expect(defaultTheme).toHaveProperty('animation')
|
|
19
|
-
expect(defaultTheme).toHaveProperty('colorScheme')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('ThemeContext is a valid React context', () => {
|
|
23
|
-
expect(ThemeContext).toHaveProperty('Provider')
|
|
24
|
-
expect(ThemeContext).toHaveProperty('Consumer')
|
|
25
|
-
})
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
describe('ThemeProvider logic', () => {
|
|
29
|
-
it('forcedColorScheme dark resolves dark tokens', () => {
|
|
30
|
-
const theme = resolveTokens(presets.zinc, 'dark')
|
|
31
|
-
expect(theme.colorScheme).toBe('dark')
|
|
32
|
-
expect(theme.colors.background).toBe('#070709')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('forcedColorScheme light resolves light tokens', () => {
|
|
36
|
-
const theme = resolveTokens(presets.zinc, 'light')
|
|
37
|
-
expect(theme.colorScheme).toBe('light')
|
|
38
|
-
expect(theme.colors.background).toBe('#ffffff')
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('each preset resolves to different primary colors', () => {
|
|
42
|
-
const themes = Object.entries(presets).map(([name, preset]) => ({
|
|
43
|
-
name,
|
|
44
|
-
primary: resolveTokens(preset, 'dark').colors.primary,
|
|
45
|
-
}))
|
|
46
|
-
const primaries = new Set(themes.map((t) => t.primary))
|
|
47
|
-
// At least 3 unique primaries across 4 presets (zinc and slate may share)
|
|
48
|
-
expect(primaries.size).toBeGreaterThanOrEqual(3)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('overrides are applied per-mode', () => {
|
|
52
|
-
const darkOverrides = { colors: { primary: '#ff0000' } }
|
|
53
|
-
const darkTheme = resolveTokens(presets.zinc, 'dark', darkOverrides)
|
|
54
|
-
const lightTheme = resolveTokens(presets.zinc, 'light')
|
|
55
|
-
expect(darkTheme.colors.primary).toBe('#ff0000')
|
|
56
|
-
expect(lightTheme.colors.primary).not.toBe('#ff0000')
|
|
57
|
-
})
|
|
58
|
-
})
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { defaultTheme, ThemeContext } from '../theme/ThemeContext'
|
|
3
|
+
import { resolveTokens, zinc, presets } from '../tokens'
|
|
4
|
+
|
|
5
|
+
describe('ThemeContext', () => {
|
|
6
|
+
it('defaultTheme is zinc light', () => {
|
|
7
|
+
const expected = resolveTokens(zinc, 'light')
|
|
8
|
+
expect(defaultTheme.colorScheme).toBe('light')
|
|
9
|
+
expect(defaultTheme.colors.background).toBe(expected.colors.background)
|
|
10
|
+
expect(defaultTheme.colors.foreground).toBe(expected.colors.foreground)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('defaultTheme has all required token categories', () => {
|
|
14
|
+
expect(defaultTheme).toHaveProperty('colors')
|
|
15
|
+
expect(defaultTheme).toHaveProperty('spacing')
|
|
16
|
+
expect(defaultTheme).toHaveProperty('radius')
|
|
17
|
+
expect(defaultTheme).toHaveProperty('typography')
|
|
18
|
+
expect(defaultTheme).toHaveProperty('animation')
|
|
19
|
+
expect(defaultTheme).toHaveProperty('colorScheme')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('ThemeContext is a valid React context', () => {
|
|
23
|
+
expect(ThemeContext).toHaveProperty('Provider')
|
|
24
|
+
expect(ThemeContext).toHaveProperty('Consumer')
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('ThemeProvider logic', () => {
|
|
29
|
+
it('forcedColorScheme dark resolves dark tokens', () => {
|
|
30
|
+
const theme = resolveTokens(presets.zinc, 'dark')
|
|
31
|
+
expect(theme.colorScheme).toBe('dark')
|
|
32
|
+
expect(theme.colors.background).toBe('#070709')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('forcedColorScheme light resolves light tokens', () => {
|
|
36
|
+
const theme = resolveTokens(presets.zinc, 'light')
|
|
37
|
+
expect(theme.colorScheme).toBe('light')
|
|
38
|
+
expect(theme.colors.background).toBe('#ffffff')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('each preset resolves to different primary colors', () => {
|
|
42
|
+
const themes = Object.entries(presets).map(([name, preset]) => ({
|
|
43
|
+
name,
|
|
44
|
+
primary: resolveTokens(preset, 'dark').colors.primary,
|
|
45
|
+
}))
|
|
46
|
+
const primaries = new Set(themes.map((t) => t.primary))
|
|
47
|
+
// At least 3 unique primaries across 4 presets (zinc and slate may share)
|
|
48
|
+
expect(primaries.size).toBeGreaterThanOrEqual(3)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('overrides are applied per-mode', () => {
|
|
52
|
+
const darkOverrides = { colors: { primary: '#ff0000' } }
|
|
53
|
+
const darkTheme = resolveTokens(presets.zinc, 'dark', darkOverrides)
|
|
54
|
+
const lightTheme = resolveTokens(presets.zinc, 'light')
|
|
55
|
+
expect(darkTheme.colors.primary).toBe('#ff0000')
|
|
56
|
+
expect(lightTheme.colors.primary).not.toBe('#ff0000')
|
|
57
|
+
})
|
|
58
|
+
})
|