├── .eslintrc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── example ├── .expo-shared │ └── assets.json ├── .gitignore ├── App.tsx ├── app.json ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png ├── babel.config.js ├── entry.js ├── metro.config.js ├── package.json ├── src │ ├── Example.tsx │ ├── PerfTest.tsx │ ├── components │ │ ├── Button.tsx │ │ ├── ColorMode.tsx │ │ ├── Heading.tsx │ │ ├── Media.tsx │ │ ├── Spacer.tsx │ │ ├── Stack.tsx │ │ ├── Text.tsx │ │ ├── index.ts │ │ └── utils.ts │ ├── styles │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── styled.ts │ │ └── utils.ts │ └── type-test.tsx ├── tsconfig.json ├── webpack.config.js └── yarn.lock ├── media └── logo.jpg ├── package.json ├── src ├── internals │ ├── constants.js │ ├── index.js │ └── utils.js ├── tests │ └── index.test.tsx └── types │ ├── config.d.ts │ ├── css-util.d.ts │ ├── index.d.ts │ ├── react-native.d.ts │ ├── stitches.d.ts │ ├── styled-component.d.ts │ ├── theme.d.ts │ └── util.d.ts ├── tsconfig.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:react/recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier", 6 | "plugin:prettier/recommended" 7 | ], 8 | "parser": "@typescript-eslint/parser", 9 | "plugins": [ 10 | "@typescript-eslint", 11 | "import", 12 | "prettier", 13 | "react", 14 | "react-hooks" 15 | ], 16 | "env": { 17 | "es6": true, 18 | "browser": true 19 | }, 20 | "settings": { 21 | "react": { 22 | "version": "detect" 23 | } 24 | }, 25 | "rules": { 26 | "react/jsx-uses-react": "off", 27 | "react/react-in-jsx-scope": "off", 28 | "react/prop-types": 0, 29 | "react-hooks/rules-of-hooks": "error", 30 | "react-hooks/exhaustive-deps": "warn", 31 | "no-var": "error", 32 | "no-console": "off", 33 | "no-unused-vars": "off", 34 | "@typescript-eslint/no-unused-vars": [ 35 | "error", 36 | { "argsIgnorePattern": "^_" } 37 | ], 38 | "semi": ["error", "always"], 39 | "prefer-const": "error", 40 | "prefer-arrow-callback": "error", 41 | "@typescript-eslint/explicit-function-return-type": "off", 42 | "@typescript-eslint/explicit-module-boundary-types": "off", 43 | "@typescript-eslint/explicit-member-accessibility": "off", 44 | "@typescript-eslint/no-explicit-any": "off", 45 | "@typescript-eslint/no-use-before-define": "off", 46 | "@typescript-eslint/prefer-interface": "off", 47 | "@typescript-eslint/ban-types": "off", 48 | "@typescript-eslint/array-type": "off", 49 | "@typescript-eslint/ban-ts-comment": "off", 50 | "eslint-comments/no-unlimited-disable": "off", 51 | "import/order": [ 52 | "error", 53 | { 54 | "groups": [ 55 | ["builtin", "external", "internal"], 56 | ["parent", "sibling", "index"] 57 | ] 58 | } 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: actions/setup-node@v2 9 | with: 10 | node-version: '15' 11 | - name: Install modules 12 | run: yarn 13 | - name: Lint 14 | run: yarn lint 15 | - name: Test 16 | run: yarn test --ci --coverage --maxWorkers=2 17 | - name: Build 18 | run: yarn build 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | .vscode 6 | lib/ 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Teemu Taskula 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Stitches Native logo 3 |

4 | 5 |

6 |

7 | Stitches Native 8 |

9 | · 10 | React Native implementation of the popular CSS-in-JS library Stitches 11 | · 12 |
13 |
14 |
15 | 16 | ## Installation 17 | 18 | ```sh 19 | npm install stitches-native 20 | ``` 21 | 22 | or if you use `yarn`: 23 | 24 | ```sh 25 | yarn add stitches-native 26 | ``` 27 | 28 | ## Documentation 29 | 30 | For the most part Stitches Native behaves exactly as Stitches so you should follow the [Stitches documentation](https://stitches.dev/) to learn the basic principles and how to setup everything. 31 | 32 | ## Differences 33 | 34 | Due to the inherit differences between the Web and native platforms (iOS + Android) the implementation of Stitches Native differs slightly from the original Web version of Stitches. 35 | 36 | First of all, CSS in React Native doesn't have CSS Variables, cascade, inheritance, keyframes, pseudo elements/classes, or global styles which means that some features that are available in Stitches are not possible to implement in Stitches Native. 37 | 38 | Below you can see a list of all supported and unsupported features of Stitches Native. 39 | 40 | ### Feature comparison 41 | 42 | | Feature | Supported | 43 | | --------------------- | ----------------------------------------- | 44 | | `styled` | ✅ | 45 | | `createStitches` | ✅ | 46 | | `defaultThemeMap` | ✅ | 47 | | `css` | ✅ _(Simplified version)_ | 48 | | `theme` | ✅ _(Use `useTheme` in components/hooks)_ | 49 | | `createTheme` | ✅ _(Only returned by `createStitches`)_ | 50 | | `useTheme` | 🆕 (Stitches Native specific) | 51 | | `ThemeProvider` | 🆕 (Stitches Native specific) | 52 | | `styled().attrs()` | 🆕 (Stitches Native specific) | 53 | | `globalCss` | ❌ _(No global styles in RN)_ | 54 | | `keyframes` | ❌ _(No CSS keyframes in RN)_ | 55 | | `getCssText` | ❌ _(SSR not applicable to RN)_ | 56 | | Nesting | ❌ _(No CSS cascade in RN)_ | 57 | | Selectors | ❌ _(No CSS selectors in RN)_ | 58 | | Locally scoped tokens | ❌ _(No CSS variables in RN)_ | 59 | | Pseudo elements | ❌ _(No pseudo elements/classes in RN)_ | 60 | 61 | ### Using `createStitches` function 62 | 63 | The `createStitches` function doesn't need `prefix` or `insertionMethod` since they are not used in the native implementation. 64 | 65 | ```js 66 | import { createStitches } from 'stitches-native'; 67 | 68 | createStitches({ 69 | theme: object, 70 | media: object, 71 | utils: object, 72 | themeMap: object, 73 | }); 74 | ``` 75 | 76 | The return value of `createStitches` doesn't include `globalCss`, `keyframes`, or `getCssText` since they are not available in native platforms. React Native doesn't have any CSS keyframes based animations and all animations should be handled by the [Animated API](https://reactnative.dev/docs/animated) or with libraries such as [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated). 77 | 78 | The return value of `createStitches` consist of the following: 79 | 80 | ```js 81 | const { styled, css, theme, createTheme, useTheme, ThemeProvider, config } = 82 | createStitches({ 83 | /*...*/ 84 | }); 85 | ``` 86 | 87 | #### Supported token types 88 | 89 | The following token types are supported in React Native: `borderStyles`, `borderWidths`, `colors`, `fonts`, `fontSizes`, `fontWeights`, `letterSpacings`, `lineHeights`, `radii`, `sizes`, `space`, `zIndices`. 90 | 91 | The only unsupported token types are `shadows` and `transitions`. Shadows in React Native cannot be expressed with a single string token like on the Web where CSS `box-shadow` accepts a string that fully describes the shadow. In React Native shadows are defined differently on iOS and Android. On [iOS](https://reactnative.dev/docs/shadow-props) you need to set the various shadow properties separately: 92 | 93 | ```ts 94 | shadowOffset: { 95 | width: number, 96 | height: number 97 | }, 98 | shadowOpacity: number, 99 | shadowRadius: number 100 | ``` 101 | 102 | On [Android](https://developer.android.com/training/material/shadows-clipping#Elevation) there is a completely different elevation system that doesn't let you alter individual shadow properties but instead you have to set a single number as the elevation level: 103 | 104 | ```ts 105 | elevation: number; 106 | ``` 107 | 108 | So, instead of having shadows as part of the design tokens in the `theme` we can quite easily define shadow utilities inside `utils`: 109 | 110 | ```js 111 | createStitches({ 112 | utils: { 113 | shadow: (level: 'small' | 'medium' | 'large') => { 114 | return { 115 | small: { 116 | elevation: 2, 117 | shadowOffset: { width: 0, height: 1 }, 118 | shadowRadius: 3, 119 | shadowOpacity: 0.1, 120 | shadowColor: '#000', 121 | }, 122 | medium: { 123 | elevation: 5, 124 | shadowOffset: { width: 0, height: 3 }, 125 | shadowRadius: 6, 126 | shadowOpacity: 0.2, 127 | shadowColor: '#000', 128 | }, 129 | large: { 130 | elevation: 10, 131 | shadowOffset: { width: 0, height: 6 }, 132 | shadowRadius: 12, 133 | shadowOpacity: 0.4, 134 | shadowColor: '#000', 135 | }, 136 | }[level]; 137 | }, 138 | }, 139 | }); 140 | ``` 141 | 142 | You can then use the shadow util like this: 143 | 144 | ```js 145 | const Comp = styled('View', { 146 | shadow: 'medium', 147 | }); 148 | ``` 149 | 150 | The other unsupported token type is `transitions` which conflicts with how animations are handled in React Native. Read more about animations in the [Animations docs](https://reactnative.dev/docs/animations). 151 | 152 | ### Using `css` helper 153 | 154 | Unlike on the Web there is no concept of `className` in React Native so the `css` function is basically an identity function providing only TS types for the style object and returning exactly the same object back (or if given multiple objects merges them together). The returned object can be appended after the first argument of a styled component. 155 | 156 | ```jsx 157 | const styles = css({ 158 | backgroundColor: '$background', // <- get autocomplete for theme values 159 | }); 160 | 161 | const SomeComp = styled( 162 | 'View', 163 | { 164 | /* ...other styles... */ 165 | }, 166 | styles // <- you can add as many shared styles as you want 167 | ); 168 | 169 | ; 170 | ``` 171 | 172 | ### Theming with `createTheme` 173 | 174 | Stitches Native handles theming differently than Stitches. Since there are no CSS Variables in React Native theming is handled via React Context in a similar way as other CSS-in-JS libraries such as [styled-components](https://styled-components.com/docs/advanced#theming) handle theming. 175 | 176 | ```tsx 177 | const { theme, createTheme, ThemeProvider } = createStitches({ 178 | colors: { 179 | background: '#fff', 180 | text: '#000', 181 | }, 182 | }); 183 | 184 | const darkTheme = createTheme({ 185 | colors: { 186 | background: '#000', 187 | text: '#fff', 188 | }, 189 | }); 190 | 191 | function App() { 192 | // In a real world scenario this value should probably live in React Context 193 | const [darkMode, setDarkMode] = useState(false); 194 | 195 | return ( 196 | 197 | {/*...*/} 198 | 199 | ); 200 | } 201 | ``` 202 | 203 | ### Accessing the theme 204 | 205 | You can get the current theme via the `useTheme` hook: 206 | 207 | ```tsx 208 | import { useTheme } from '../your-stitches-config'; 209 | 210 | function Example() { 211 | const theme = useTheme(); 212 | 213 | // Access theme tokens 214 | // theme.colors|space|radii|etc.x 215 | 216 | return ( 217 | {/*...*/} 218 | ); 219 | } 220 | ``` 221 | 222 | ### Typing token aliases 223 | 224 | Stitches Native supports theme token aliases the same way as Stitches with a minor difference related to TypeScript support: 225 | 226 | ```ts 227 | createStitches({ 228 | colors: { 229 | black: '#000', 230 | primary: '$black' as const, 231 | }, 232 | space: { 233 | 1: 8, 234 | 2: 16, 235 | 3: 32, 236 | max: '$3' as const, 237 | }, 238 | }); 239 | ``` 240 | 241 | Note the usage of `as const` for token alias values. It is required if you want to have the correct type for the theme token value when accessing it via `useTheme` hook: 242 | 243 | ```tsx 244 | import { useTheme } from '../your-stitches-config'; 245 | 246 | function Example() { 247 | const theme = useTheme(); 248 | 249 | const x = theme.colors.primary; // <-- type of `x` is `string` 250 | const y = theme.space.max; // <--- type of `y` is `number` even though the value in the theme is `'$3'`, yey 😎 251 | 252 | return {/*...*/}; 253 | } 254 | ``` 255 | 256 | > ⚠️ NOTE: without `as const` the type of the token will always be `string`! 257 | 258 | For `string` type tokens, you don't necessarily need to use `as const` since the types align correctly without it, but you might want to do so anyway to be consistent. 259 | 260 | ### Responsive styles with `media` 261 | 262 | Responsive styles are not very common in React Native applications since you usually have a clearly constrained device environment where the app is used. However, some times you might need to tweak a style for very small or large phones or build an app that needs to adapt to tablet devices. For these use cases Stitches Native has support for two kinds of responsive styles: 263 | 264 | 1. Device types based media flags 265 | 2. Device dimensions based media queries 266 | 267 | #### Device types based media flags 268 | 269 | Simple boolean flags in the `media` config can be used to distinguish between device types, eg. phone vs. tablet. You can utilize [`getDeviceType()`](https://github.com/react-native-device-info/react-native-device-info#getDeviceType) or [`isTablet()`](https://github.com/react-native-device-info/react-native-device-info#istablet) from [react-native-device-info](https://github.com/react-native-device-info/react-native-device-info) to get the device type. 270 | 271 | ```js 272 | const isTablet = DeviceInfo.isTablet(); 273 | 274 | const { ... } = createStitches({ 275 | media: { 276 | phone: !isTablet, 277 | tablet: isTablet, 278 | }, 279 | }); 280 | ``` 281 | 282 | Then you can apply different prop values for variants of a styled components based on the device type: 283 | 284 | ```tsx 285 | const ButtonText = styled('Text', { 286 | // base styles 287 | 288 | variants: { 289 | color: { 290 | primary: { 291 | color: '$primary', 292 | }, 293 | secondary: { 294 | color: '$secondary', 295 | }, 296 | }, 297 | }, 298 | }); 299 | 300 | 301 | Hello 302 | ; 303 | ``` 304 | 305 | #### Device dimensions based media queries 306 | 307 | It's also possible to have a more Web-like breakpoint system based on the dimensions of the device. The syntax for the queries follows the CSS [range queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries#syntax_improvements_in_level_4) syntax which means that there is no need to use `min-width` or `max-width`. 308 | 309 | Examples of supported range queries: 310 | 311 | - `(width > 750px)` 312 | - `(width >= 750px)` 313 | - `(width < 1080px)` 314 | - `(width <= 1080px)` 315 | - `(750px > width >= 1080px)` 316 | 317 | > ⚠️ NOTE: Only width based media queries are currently supported. 318 | 319 | ```js 320 | const { ... } = createStitches({ 321 | media: { 322 | md: '(width >= 750px)', 323 | lg: '(width >= 1080px)', 324 | xl: '(width >= 1284px)', 325 | xxl: '(width >= 1536px)', 326 | }, 327 | }); 328 | ``` 329 | 330 | > ⚠️ NOTE: The order of the media query keys matters and the responsive styles are applied in the order determined by `Object.entries` method. 331 | 332 | Using media queries works the same way as device type flags: 333 | 334 | ```tsx 335 | const ButtonText = styled('Text', { 336 | // base styles 337 | 338 | variants: { 339 | color: { 340 | primary: { 341 | color: '$primary', 342 | }, 343 | secondary: { 344 | color: '$secondary', 345 | }, 346 | }, 347 | }, 348 | }); 349 | 350 | 357 | Hello 358 | ; 359 | ``` 360 | 361 | ### Additional props with `.attrs` 362 | 363 | In React Native it is quite common that a component exposes props (other than `style`) that accept a style object - a good example of this is the `ScrollView` component that has `contentContainerStyle` prop. Using theme tokens with these kind of props can be accomplished with the `useTheme` hook: 364 | 365 | ```tsx 366 | function Comp() { 367 | const theme = useTheme(); 368 | 369 | return ( 370 | 371 | {/* ... */} 372 | 373 | ); 374 | } 375 | 376 | const ScrollView = styled('ScrollView', { 377 | flex: 1, 378 | }); 379 | ``` 380 | 381 | This approach is fine but a bit convoluted since you have to import a hook just to access the theme tokens. There is a better way with the chainable `.attrs` method which can be used to attach additional props to a Stitches styled component (this method was popularized by [styled-components](https://styled-components.com/docs/api#attrs)). 382 | 383 | > ⚠️ NOTE: this method does not exist in the original Web version of Stitches. 384 | 385 | ```tsx 386 | function Example() { 387 | return {/*...*/}; 388 | } 389 | 390 | const ScrollView = styled('ScrollView', { 391 | flex: 1, 392 | }).attrs((props) => ({ 393 | contentContainerStyle: { 394 | padding: props.theme.space[2], 395 | }, 396 | })); 397 | ``` 398 | 399 | It is also possible to access the variants of the component within `.attrs`: 400 | 401 | ```tsx 402 | function Example() { 403 | return {/*...*/}; 404 | } 405 | 406 | const ScrollView = styled('ScrollView', { 407 | flex: 1, 408 | variants: { 409 | spacious: { 410 | true: { 411 | // some styles... 412 | }, 413 | false: { 414 | // some styles... 415 | }, 416 | }, 417 | }, 418 | }).attrs((props) => ({ 419 | contentContainerStyle: { 420 | padding: props.theme.space[props.spacious ? 4 : 2], 421 | }, 422 | })); 423 | ``` 424 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /example/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import 'react-native-gesture-handler'; 2 | import { ColorModeProvider } from './src/components'; 3 | import Example from './src/Example'; 4 | 5 | export function App() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "splash": { 9 | "image": "./assets/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#ffffff" 12 | }, 13 | "updates": { 14 | "fallbackToCacheTimeout": 0 15 | }, 16 | "assetBundlePatterns": ["**/*"], 17 | "ios": { 18 | "supportsTablet": true 19 | }, 20 | "android": { 21 | "adaptiveIcon": { 22 | "foregroundImage": "./assets/adaptive-icon.png", 23 | "backgroundColor": "#FFFFFF" 24 | } 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Temzasse/stitches-native/bd795a8d62007aa505463fdaa8e0d5258f668026/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Temzasse/stitches-native/bd795a8d62007aa505463fdaa8e0d5258f668026/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Temzasse/stitches-native/bd795a8d62007aa505463fdaa8e0d5258f668026/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Temzasse/stitches-native/bd795a8d62007aa505463fdaa8e0d5258f668026/example/assets/splash.png -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: ['react-native-reanimated/plugin'], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /example/entry.js: -------------------------------------------------------------------------------- 1 | import 'expo/build/Expo.fx'; 2 | import { AppRegistry, Platform } from 'react-native'; 3 | import withExpoRoot from 'expo/build/launch/withExpoRoot'; 4 | import { createRoot } from 'react-dom/client'; 5 | import { App } from './App'; 6 | 7 | AppRegistry.registerComponent('main', () => withExpoRoot(App)); 8 | 9 | // TODO: should we have separate `index.web.js`? 10 | // Also is should we use `registerRootComponent`? 11 | // https://docs.expo.dev/workflow/web/ 12 | 13 | if (Platform.OS === 'web') { 14 | const rootTag = createRoot( 15 | document.getElementById('root') ?? document.getElementById('main') 16 | ); 17 | 18 | const RootComponent = withExpoRoot(App); 19 | 20 | rootTag.render(); 21 | } 22 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const path = require('path'); 3 | const { mapValues } = require('lodash'); 4 | const { getDefaultConfig } = require('expo/metro-config'); 5 | 6 | const packagesRelative = { 7 | 'stitches-native': '../src/internals', 8 | }; 9 | 10 | const packages = mapValues(packagesRelative, (relativePath) => 11 | path.resolve(relativePath), 12 | ); 13 | 14 | function createMetroConfiguration(projectPath) { 15 | projectPath = path.resolve(projectPath); 16 | 17 | const defaultConfig = getDefaultConfig(projectPath); 18 | 19 | const watchFolders = [ 20 | ...Object.values(packages), 21 | ...defaultConfig.watchFolders, 22 | ]; 23 | 24 | const extraNodeModules = { 25 | ...packages, 26 | ...defaultConfig.resolver.extraNodeModules, 27 | }; 28 | 29 | const extraNodeModulesProxy = new Proxy(extraNodeModules, { 30 | get: (target, name) => { 31 | if (target[name]) { 32 | return target[name]; 33 | } else { 34 | return path.join(projectPath, `node_modules/${name}`); 35 | } 36 | }, 37 | }); 38 | 39 | return { 40 | ...defaultConfig, 41 | projectRoot: projectPath, 42 | watchFolders, 43 | resolver: { 44 | ...defaultConfig.resolver, 45 | extraNodeModules: extraNodeModulesProxy, 46 | }, 47 | }; 48 | } 49 | 50 | module.exports = createMetroConfiguration(__dirname); 51 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "entry.js", 5 | "private": true, 6 | "scripts": { 7 | "start": "expo start", 8 | "start:clean": "expo start --clear", 9 | "android": "expo start --android", 10 | "ios": "expo start --ios", 11 | "web": "expo start --web", 12 | "eject": "expo eject" 13 | }, 14 | "dependencies": { 15 | "expo": "^46.0.16", 16 | "expo-device": "~4.3.0", 17 | "expo-status-bar": "~1.4.0", 18 | "lodash.merge": "4.6.2", 19 | "react": "18.0.0", 20 | "react-dom": "18.0.0", 21 | "react-native": "0.69.6", 22 | "react-native-gesture-handler": "~2.5.0", 23 | "react-native-reanimated": "~2.9.1", 24 | "react-native-web": "~0.18.7", 25 | "stitches-native": "link:../" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "7.12.9", 29 | "@expo/webpack-config": "^0.17.0", 30 | "@types/react": "^18.0.24", 31 | "@types/react-native": "^0.70.6", 32 | "typescript": "4.7.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/src/Example.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from 'react-native'; 2 | import { StatusBar } from 'expo-status-bar'; 3 | import { useState } from 'react'; 4 | import { Stack, Text, useColorMode, Media, Heading } from './components'; 5 | import { styled } from './styles'; 6 | 7 | export default function Example() { 8 | const { toggleColorMode, colorMode } = useColorMode(); 9 | const [example, changeExample] = useState(false); 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | Example app 17 | 18 | Switch Examples 19 | 20 | 21 | 22 | {example && ( 23 | 24 | Variants 25 | Heading 26 | Heading 27 | Heading 28 | Heading 29 | 37 | Compound Variants 38 | 39 | 40 | Heading 41 | 42 | 43 | Heading 44 | 45 | 46 | Heading 47 | 48 | 49 | Heading 50 | 51 | 52 | )} 53 | {!example && ( 54 | <> 55 | 56 | {Array.from({ length: 5 }).map((_, i) => ( 57 | 58 | 59 | 60 | Box {i + 1} 61 | 62 | 63 | XXX 64 | 65 | 66 | YYY 67 | 68 | 69 | 70 | ))} 71 | 72 | 73 | 82 | Font size and color should change as viewport changes 83 | 84 | 85 | 86 | Toggle color mode 87 | 91 | 92 | 93 | )} 94 | 95 | 96 | 97 | 98 | 99 | ); 100 | } 101 | 102 | const Wrapper = styled('SafeAreaView', { 103 | flex: 1, 104 | backgroundColor: '$background', 105 | }); 106 | 107 | const Content = styled('ScrollView', { 108 | flex: 1, 109 | }).attrs((p) => ({ 110 | contentContainerStyle: { 111 | padding: p.theme.space[2], 112 | }, 113 | })); 114 | 115 | const Box = styled('View', { 116 | minHeight: 100, 117 | backgroundColor: '$primaryMuted', 118 | flexCenter: 'row', 119 | borderRadius: '$md', 120 | }); 121 | -------------------------------------------------------------------------------- /example/src/PerfTest.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { Stack } from './components'; 3 | import { styled } from './styles'; 4 | 5 | let measured = false; 6 | 7 | export default function PerfTest() { 8 | const start = useMemo(() => new Date(), []); 9 | 10 | return ( 11 | { 13 | if (!measured) { 14 | measured = true; 15 | console.log( 16 | `Time taken: ${new Date().getTime() - start.getTime()} ms` 17 | ); 18 | } 19 | }} 20 | > 21 | 22 | 23 | {Array.from({ length: 1000 }).map((_, i) => ( 24 | 25 | {i + 1} 26 | 27 | ))} 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | const Wrapper = styled('SafeAreaView', { 35 | flex: 1, 36 | backgroundColor: '$background', 37 | }); 38 | 39 | const Content = styled('ScrollView', { 40 | flex: 1, 41 | }).attrs((p) => ({ 42 | contentContainerStyle: { 43 | padding: p.theme.space[2], 44 | }, 45 | })); 46 | 47 | const Box = styled('View', { 48 | minHeight: 100, 49 | backgroundColor: '$primaryMuted', 50 | flexCenter: 'row', 51 | borderRadius: '$md', 52 | }); 53 | 54 | const BoxText = styled('Text', { 55 | color: '$primaryText', 56 | }); 57 | -------------------------------------------------------------------------------- /example/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import type * as Stitches from 'stitches-native'; 2 | import { styled } from '../styles'; 3 | import { Text } from './Text'; 4 | 5 | type StyledButtonVariants = Stitches.VariantProps; 6 | 7 | type Props = StyledButtonVariants & { 8 | children: string; 9 | }; 10 | 11 | export function Button({ children, ...props }: Props) { 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | 19 | const StyledButton = styled('TouchableOpacity', { 20 | justifyContent: 'center', 21 | alignItems: 'center', 22 | borderRadius: 999, 23 | minWidth: 100, 24 | backgroundColor: '$primary', 25 | shadow: 'medium', 26 | 27 | variants: { 28 | variant: { 29 | primary: { 30 | backgroundColor: '$primary', 31 | }, 32 | secondary: { 33 | backgroundColor: '$secondary', 34 | }, 35 | }, 36 | size: { 37 | small: { 38 | height: 32, 39 | paddingHorizontal: '$2', 40 | }, 41 | large: { 42 | height: 44, 43 | paddingHorizontal: '$3', 44 | }, 45 | }, 46 | outlined: { 47 | true: { 48 | borderWidth: 1, 49 | shadow: 'none', 50 | }, 51 | }, 52 | }, 53 | compoundVariants: [ 54 | { 55 | variant: 'primary', 56 | outlined: true, 57 | css: { 58 | borderColor: '$primary', 59 | backgroundColor: 'transparent', 60 | }, 61 | }, 62 | { 63 | variant: 'secondary', 64 | outlined: true, 65 | css: { 66 | borderColor: '$secondary', 67 | backgroundColor: 'transparent', 68 | }, 69 | }, 70 | ], 71 | defaultVariants: { 72 | variant: 'primary', 73 | size: 'large', 74 | }, 75 | }); 76 | -------------------------------------------------------------------------------- /example/src/components/ColorMode.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | useContext, 4 | ReactNode, 5 | useState, 6 | useCallback, 7 | } from 'react'; 8 | 9 | import { theme as lightTheme, darkTheme, ThemeProvider } from '../styles'; 10 | 11 | type ColorMode = 'light' | 'dark'; 12 | 13 | type ContextValue = { 14 | colorMode: ColorMode; 15 | setColorMode: (t: ColorMode) => void; 16 | toggleColorMode: () => void; 17 | }; 18 | 19 | const ColorModeContext = createContext(undefined); 20 | 21 | export function ColorModeProvider({ children }: { children: ReactNode }) { 22 | const [colorMode, setColorMode] = useState('light'); 23 | const theme = colorMode === 'light' ? lightTheme : darkTheme; 24 | 25 | const toggleColorMode = useCallback(() => { 26 | setColorMode((p) => (p === 'dark' ? 'light' : 'dark')); 27 | }, []); 28 | 29 | return ( 30 | 33 | {children} 34 | 35 | ); 36 | } 37 | 38 | export const useColorMode = () => { 39 | const context = useContext(ColorModeContext); 40 | if (!context) throw new Error('Missing ColorModeProvider!'); 41 | return context; 42 | }; 43 | -------------------------------------------------------------------------------- /example/src/components/Heading.tsx: -------------------------------------------------------------------------------- 1 | import { TextProps as RNTextProps } from 'react-native'; 2 | import { styled, css } from '../styles'; 3 | 4 | export const Typography = styled('Text', { 5 | color: '$text', 6 | fontSizeRem: 1, 7 | }); 8 | 9 | type HeadingSize = 'h1' | 'h2' | 'h3' | 'h4' | 'h5'; 10 | 11 | export type HeadingProps = RNTextProps & { 12 | heading?: HeadingSize; 13 | }; 14 | 15 | const underLinedStyle = css({ 16 | compoundVariants: [ 17 | { 18 | heading: 'h5', 19 | underlined: true, 20 | css: { 21 | borderBottomColor: 'black', 22 | borderBottomWidth: 1, 23 | }, 24 | }, 25 | { 26 | heading: 'h4', 27 | underlined: true, 28 | css: { 29 | borderBottomColor: 'red', 30 | borderBottomWidth: 1, 31 | }, 32 | }, 33 | { 34 | heading: 'h3', 35 | underlined: true, 36 | css: { 37 | borderBottomColor: 'blue', 38 | borderBottomWidth: 1, 39 | }, 40 | }, 41 | { 42 | heading: 'h2', 43 | underlined: true, 44 | css: { 45 | borderBottomColor: 'green', 46 | borderBottomWidth: 1, 47 | }, 48 | }, 49 | { 50 | heading: 'h1', 51 | underlined: true, 52 | css: { 53 | borderBottomColor: 'purple', 54 | borderBottomWidth: 1, 55 | }, 56 | }, 57 | { 58 | // NOTE: To check default variants 59 | heading: 'h1', 60 | underlined: false, 61 | css: { 62 | marginBottom: 2, 63 | }, 64 | }, 65 | ], 66 | }); 67 | 68 | export const Heading = styled( 69 | 'Text', 70 | { 71 | fontWeight: 'bold', 72 | color: '$plainText', 73 | variants: { 74 | heading: { 75 | h5: { fontSizeRem: 1.0, color: 'black' }, 76 | h4: { fontSizeRem: 1.1, color: 'red' }, 77 | h3: { fontSizeRem: 1.25, color: 'blue' }, 78 | h2: { fontSizeRem: 1.5, color: 'green' }, 79 | h1: { fontSizeRem: 2.5, color: 'purple' }, 80 | }, 81 | underlined: { 82 | true: { 83 | paddingRight: 4, 84 | paddingLeft: 4, 85 | }, 86 | }, 87 | }, 88 | defaultVariants: { 89 | heading: 'h1', 90 | underlined: false, 91 | }, 92 | }, 93 | // TODO: fix this! Native `Text` cannot have a border bottom! 94 | // The example needs to wrap the text with a `View` and apply the border bottom to that. 95 | underLinedStyle 96 | ).attrs(() => ({ 97 | accessibilityRole: 'text', 98 | })); 99 | -------------------------------------------------------------------------------- /example/src/components/Media.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '../styles'; 2 | 3 | export const Media = styled('Text', { 4 | color: '$text', 5 | '@xxl': { 6 | fontSize: 64, 7 | }, 8 | '@xl': { 9 | fontSize: 48, 10 | }, 11 | '@lg': { 12 | fontSize: 32, 13 | }, 14 | '@md': { 15 | fontSize: 24, 16 | }, 17 | '@sm': { 18 | fontSize: 12, 19 | }, 20 | marginTopRem: 1, 21 | marginBottomRem: 1, 22 | variants: { 23 | color: { 24 | primary: { color: 'red' }, 25 | secondary: { color: 'blue' }, 26 | third: { color: 'purple' }, 27 | forth: { color: 'green' }, 28 | fifth: { color: 'black' }, 29 | }, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /example/src/components/Spacer.tsx: -------------------------------------------------------------------------------- 1 | import { styled, themeProp } from '../styles'; 2 | 3 | export const Spacer = styled('View', { 4 | flexShrink: 0, 5 | variants: { 6 | ...themeProp('size', 'space', (value) => ({ 7 | width: `$space${value}`, 8 | height: `$space${value}`, 9 | })), 10 | axis: { 11 | x: { height: 'auto' }, 12 | y: { width: 'auto' }, 13 | }, 14 | debug: { 15 | true: { backgroundColor: 'red' }, 16 | false: { backgroundColor: 'transparent' }, 17 | }, 18 | }, 19 | }); 20 | 21 | // @ts-ignore 22 | Spacer.__SPACER__ = true; // This is used to detect spacers inside Stack component 23 | -------------------------------------------------------------------------------- /example/src/components/Stack.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { ViewStyle } from 'react-native'; 3 | import { Spacer } from './Spacer'; 4 | 5 | import { styled, Theme } from '../styles'; 6 | import { flattenChildren } from './utils'; 7 | 8 | type Props = { 9 | space: keyof Theme['space']; 10 | axis?: 'x' | 'y'; 11 | align?: 'center' | 'start' | 'end' | 'stretch'; 12 | justify?: 'center' | 'start' | 'end' | 'between' | 'around'; 13 | style?: ViewStyle; 14 | debug?: boolean; 15 | children: React.ReactNode; 16 | }; 17 | 18 | export function Stack({ 19 | children, 20 | axis, 21 | space, 22 | align, 23 | justify, 24 | debug, 25 | ...rest 26 | }: Props) { 27 | // Handle `React.Fragments` by flattening children 28 | const elements = flattenChildren(children).filter((e) => 29 | React.isValidElement(e) 30 | ); 31 | 32 | const lastIndex = React.Children.count(elements) - 1; 33 | 34 | return ( 35 | 36 | {elements.map((child, index) => { 37 | if (!React.isValidElement(child)) return null; 38 | 39 | const isSpacer = (child as any).type['__SPACER__']; 40 | 41 | // Just return spacers as is so that they can override the default spacing 42 | if (isSpacer) return React.cloneElement(child); 43 | 44 | const isLast = index === lastIndex; 45 | const nextElement = isLast ? null : (elements[index + 1] as any); 46 | const isNextSpacer = nextElement && nextElement.type['__SPACER__']; 47 | const shouldAddSpacing = !isLast && !isNextSpacer; 48 | 49 | return ( 50 | 51 | {React.cloneElement(child)} 52 | {shouldAddSpacing && ( 53 | 54 | )} 55 | 56 | ); 57 | })} 58 | 59 | ); 60 | } 61 | 62 | const StyledStack = styled('View', { 63 | variants: { 64 | axis: { 65 | x: { flexDirection: 'row' }, 66 | y: { flexDirection: 'column' }, 67 | }, 68 | align: { 69 | center: { alignItems: 'center' }, 70 | start: { alignItems: 'flex-start' }, 71 | end: { alignItems: 'flex-end' }, 72 | stretch: { alignItems: 'stretch' }, 73 | }, 74 | justify: { 75 | center: { justifyContent: 'center' }, 76 | start: { justifyContent: 'flex-start' }, 77 | end: { justifyContent: 'flex-end' }, 78 | between: { justifyContent: 'space-between' }, 79 | around: { justifyContent: 'space-around' }, 80 | }, 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /example/src/components/Text.tsx: -------------------------------------------------------------------------------- 1 | import { styled, themeProp } from '../styles'; 2 | 3 | export const Text = styled('Text', { 4 | color: '$text', 5 | fontSize: 16, 6 | variants: { 7 | ...themeProp('color', 'colors', (value) => ({ 8 | color: value, 9 | })), 10 | variant: { 11 | body: { typography: '$body' }, 12 | bodySmall: { typography: '$bodySmall' }, 13 | bodyExtraSmall: { typography: '$bodyExtraSmall' }, 14 | title1: { typography: '$title1' }, 15 | title2: { typography: '$title2' }, 16 | title3: { typography: '$title3' }, 17 | }, 18 | align: { 19 | left: { textAlign: 'left' }, 20 | right: { textAlign: 'right' }, 21 | center: { textAlign: 'center' }, 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: 'body', 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /example/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Text } from './Text'; 2 | export { Media } from './Media'; 3 | export { Heading } from './Heading'; 4 | export { Stack } from './Stack'; 5 | export { Spacer } from './Spacer'; 6 | export { ColorModeProvider, useColorMode } from './ColorMode'; 7 | -------------------------------------------------------------------------------- /example/src/components/utils.ts: -------------------------------------------------------------------------------- 1 | import { Children, Fragment } from 'react'; 2 | 3 | type ReactChildArray = ReturnType; 4 | 5 | export function flattenChildren(children: React.ReactNode): ReactChildArray { 6 | const childrenArray = Children.toArray(children); 7 | return childrenArray.reduce((flatChildren: ReactChildArray, child) => { 8 | if ((child as React.ReactElement).type === Fragment) { 9 | return flatChildren.concat( 10 | flattenChildren((child as React.ReactElement).props.children) 11 | ); 12 | } 13 | flatChildren.push(child); 14 | return flatChildren; 15 | }, []); 16 | } 17 | -------------------------------------------------------------------------------- /example/src/styles/helpers.ts: -------------------------------------------------------------------------------- 1 | import { theme, Theme } from './styled'; 2 | 3 | type ThemeKey = keyof Theme; 4 | 5 | export function themeProp

( 6 | prop: P, 7 | themeKey: T, 8 | getStyles: (token: string) => any 9 | ) { 10 | return Object.values(theme[themeKey]).reduce( 11 | (acc, { token }) => { 12 | acc[prop][token] = getStyles(`$${token}`); 13 | return acc; 14 | }, 15 | { [prop]: {} } 16 | ) as { 17 | [prop in P]: { [token in keyof Theme[T]]: any }; // TODO: fix any 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /example/src/styles/index.ts: -------------------------------------------------------------------------------- 1 | import * as stitches from './styled'; 2 | 3 | const { styled, css, createTheme, useTheme, theme, darkTheme, ThemeProvider } = stitches; // prettier-ignore 4 | 5 | export { Theme } from './styled'; 6 | export { themeProp } from './helpers'; 7 | export { styled, css, createTheme, useTheme, theme, darkTheme, ThemeProvider }; 8 | -------------------------------------------------------------------------------- /example/src/styles/styled.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { getDeviceTypeAsync, DeviceType } from 'expo-device'; 3 | import { createStitches } from 'stitches-native'; 4 | import type * as Stitches from 'stitches-native'; 5 | 6 | import { 7 | size, 8 | shadow, 9 | typography, 10 | flexCenter, 11 | absoluteFill, 12 | remFunction, 13 | } from './utils'; 14 | 15 | const media = { 16 | // You can provide boolean values for breakpoints when you just need to 17 | // distinguish between phone and tablet devices 18 | phone: true, 19 | tablet: false, 20 | 21 | // If you are not using Expo you should use react-native-device-info 22 | // to get the device type synchronously 23 | /* 24 | phone: true, // !DeviceInfo.isTablet() 25 | tablet: false, // DeviceInfo.isTablet() 26 | */ 27 | 28 | // You can also define min width based media queries that overlap each other 29 | // which is a commonly used technique in web development 30 | // NOTE: make sure the keys are ordered from smallest to largest screen size! 31 | md: '(width >= 750px)', 32 | lg: '(width >= 1080px)', 33 | xl: '(width >= 1284px)', 34 | xxl: '(width >= 1536px)', 35 | 36 | // It's also possible to specify ranges that don't overlap if you want to be 37 | // very precise with your media queries and don't prefer the min width based approach 38 | /* 39 | sm: '(width <= 750px)', // Small phone, eg. iPhone SE 40 | md: '(750px < width <= 1080px)', // Regular phone, eg. iPhone 6/7/8 Plus 41 | lg: '(1080px < width <= 1284px)', // Large phone, eg. iPhone 12 Pro Max 42 | xl: '(1284px < width <= 1536px)', // Regular tablet, eg. iPad Pro 9.7 43 | xxl: '(width > 1536px)', // Large tablet 44 | */ 45 | }; 46 | 47 | // This is a bit hacky but Expo doesn't have a sync way to get the device type 48 | getDeviceTypeAsync().then((deviceType) => { 49 | media.phone = deviceType === DeviceType.PHONE; 50 | media.tablet = deviceType === DeviceType.TABLET; 51 | }); 52 | 53 | const { styled, css, createTheme, config, theme, useTheme, ThemeProvider } = 54 | createStitches({ 55 | theme: { 56 | colors: { 57 | // Main palette (these should not be used directly but via aliases instead) 58 | blue100: '#ab9cf7', 59 | blue500: '#301b96', 60 | blue900: '#0D0630', 61 | green100: '#d9fff6', 62 | green500: '#8BBEB2', 63 | green900: '#384d48', 64 | black: '#000000', 65 | white: '#ffffff', 66 | gray50: '#f2f2f7', 67 | gray100: '#e5e5ea', 68 | gray200: '#d1d1d6', 69 | gray300: '#c7c7cc', 70 | gray400: '#aeaeb2', 71 | gray500: '#8e8e93', 72 | gray600: '#636366', 73 | gray700: '#48484a', 74 | gray800: '#3a3a3c', 75 | gray850: '#2c2c2e', 76 | gray900: '#1d1d1f', 77 | 78 | // Brand colors 79 | primary: '$blue500', 80 | primaryText: '$blue900', 81 | primaryMuted: '$blue100', 82 | secondary: '$green500', 83 | secondaryText: '$green900', 84 | secondaryMuted: '$green100', 85 | 86 | // Informative colors 87 | info: '#3B82F6', 88 | infoText: '#0A45A6', 89 | infoMuted: '#cfdef7', 90 | success: '#10B981', 91 | successText: '#06734E', 92 | successMuted: '#cee8df', 93 | warn: '#FBBF24', 94 | warnText: '#8a6200', 95 | warnMuted: '#f3ead1', 96 | error: '#EF4444', 97 | errorText: '#8C0606', 98 | errorMuted: '#f3d2d3', 99 | 100 | // General colors 101 | text: '$black', 102 | textInverted: '$white', 103 | border: 'rgba(150, 150, 150, 0.3)', 104 | backdrop: 'rgba(0,0,0,0.5)', 105 | background: '$white', 106 | surface: '$white', 107 | elevated: '$white', 108 | muted1: '$gray500', 109 | muted2: '$gray400', 110 | muted3: '$gray300', 111 | muted4: '$gray200', 112 | muted5: '$gray100', 113 | muted6: '$gray50', 114 | }, 115 | fontWeights: { 116 | bold: '700', 117 | semibold: '500', 118 | normal: '400', 119 | }, 120 | borderStyles: { 121 | solid: 'solid', 122 | }, 123 | borderWidths: { 124 | thin: StyleSheet.hairlineWidth, 125 | normal: 1, 126 | thick: 2, 127 | }, 128 | fontSizes: { 129 | xxs: 10, 130 | xs: 14, 131 | sm: 16, 132 | md: 18, 133 | lg: 20, 134 | xl: 24, 135 | xxl: 32, 136 | }, 137 | lineHeights: { 138 | xxs: 12, 139 | xs: 16, 140 | sm: 18, 141 | md: 20, 142 | lg: 24, 143 | xl: 28, 144 | xxl: 36, 145 | }, 146 | letterSpacings: { 147 | tight: 0.1, 148 | sparse: 1, 149 | }, 150 | zIndices: { 151 | modal: 1000, 152 | }, 153 | space: { 154 | none: 0, 155 | 1: 4, 156 | 2: 8, 157 | 3: 16, 158 | 4: 24, 159 | 5: 32, 160 | 6: 40, 161 | 7: 56, 162 | 8: 72, 163 | 9: 96, 164 | max: '$9' as const, 165 | }, 166 | sizes: { 167 | hairlineWidth: StyleSheet.hairlineWidth, 168 | }, 169 | radii: { 170 | sm: 4, 171 | md: 8, 172 | lg: 24, 173 | full: 999, 174 | }, 175 | }, 176 | utils: { 177 | size, 178 | shadow, 179 | typography, 180 | flexCenter, 181 | absoluteFill, 182 | fontSizeRem: remFunction('fontSize'), 183 | widthRem: remFunction('width'), 184 | heightRem: remFunction('height'), 185 | lineHeightRem: remFunction('lineHeight'), 186 | minWidthRem: remFunction('minWidth'), 187 | minHeightRem: remFunction('minHeight'), 188 | maxWidthRem: remFunction('maxWidth'), 189 | maxHeightRem: remFunction('maxHeight'), 190 | marginRightRem: remFunction('marginRight'), 191 | marginLeftRem: remFunction('marginLeft'), 192 | marginTopRem: remFunction('marginTop'), 193 | marginBottomRem: remFunction('marginBottom'), 194 | paddingRightRem: remFunction('paddingRight'), 195 | paddingLeftRem: remFunction('paddingLeft'), 196 | paddingTopRem: remFunction('paddingTop'), 197 | paddingBottomRem: remFunction('paddingBottom'), 198 | borderRadiusRem: remFunction('borderRadius'), 199 | }, 200 | media, 201 | }); 202 | 203 | const darkTheme = createTheme({ 204 | colors: { 205 | // Brand colors 206 | primary: '$blue500', 207 | primaryText: '$blue100', 208 | primaryMuted: '$blue900', 209 | secondary: '$green500', 210 | secondaryText: '$green100', 211 | secondaryMuted: '$green900', 212 | 213 | // Informative colors 214 | info: '#3B82F6', 215 | infoText: '#81aef7', 216 | infoMuted: '#1b2940', 217 | success: '#10B981', 218 | successText: '#1ee8a5', 219 | successMuted: '#193328', 220 | warn: '#FBBF24', 221 | warnText: '#ffc93d', 222 | warnMuted: '#40351a', 223 | error: '#EF4444', 224 | errorText: '#ff7070', 225 | errorMuted: '#3e1c1d', 226 | 227 | // General colors 228 | text: '$white', 229 | textInverted: '$black', 230 | background: '$black', 231 | backdrop: 'rgba(0,0,0,0.5)', 232 | surface: '$gray800', 233 | elevated: '$gray600', 234 | muted1: '$gray500', 235 | muted2: '$gray600', 236 | muted3: '$gray700', 237 | muted4: '$gray800', 238 | muted5: '$gray850', 239 | muted6: '$gray900', 240 | }, 241 | }); 242 | 243 | export { 244 | styled, 245 | css, 246 | createTheme, 247 | useTheme, 248 | config, 249 | theme, 250 | darkTheme, 251 | ThemeProvider, 252 | }; 253 | 254 | export type CSS = Stitches.CSS; 255 | export type Theme = typeof theme; 256 | -------------------------------------------------------------------------------- /example/src/styles/utils.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | import type * as Stitches from 'stitches-native'; 4 | 5 | export type TypographyVariant = 6 | | 'body' 7 | | 'bodySmall' 8 | | 'bodyExtraSmall' 9 | | 'title1' 10 | | 'title2' 11 | | 'title3'; 12 | 13 | type TypographyVariantVar = `$${TypographyVariant}`; 14 | 15 | // TODO: is there a way to type tokens? Using `CSS` from `styled.ts` doesn't work 16 | // because it causes a circular type dependency since `typography` is used in `utils`. 17 | const typographyVariants: { 18 | [variant in TypographyVariantVar]: CSSProperties; 19 | } = { 20 | $title1: { 21 | fontSize: '$xxl', 22 | fontWeight: '$bold', 23 | }, 24 | $title2: { 25 | fontSize: '$xl', 26 | fontWeight: '$bold', 27 | }, 28 | $title3: { 29 | fontSize: '$lg', 30 | fontWeight: '$bold', 31 | }, 32 | $body: { 33 | fontSize: '$md', 34 | fontWeight: '$normal', 35 | }, 36 | $bodySmall: { 37 | fontSize: '$sm', 38 | fontWeight: '$normal', 39 | }, 40 | $bodyExtraSmall: { 41 | fontSize: '$xs', 42 | fontWeight: '$semibold', 43 | }, 44 | }; 45 | 46 | export const typography = (value: TypographyVariantVar) => { 47 | return typographyVariants[value]; 48 | }; 49 | 50 | export const size = (value: Stitches.PropertyValue<'width'>) => ({ 51 | width: value, 52 | height: value, 53 | }); 54 | 55 | export const shadow = (level: 'none' | 'small' | 'medium' | 'large') => { 56 | return { 57 | none: { 58 | elevation: 0, 59 | shadowOffset: { width: 0, height: 0 }, 60 | shadowRadius: 0, 61 | shadowOpacity: 0, 62 | shadowColor: '#000', 63 | }, 64 | small: { 65 | elevation: 2, 66 | shadowOffset: { width: 0, height: 1 }, 67 | shadowRadius: 3, 68 | shadowOpacity: 0.1, 69 | shadowColor: '#000', 70 | }, 71 | medium: { 72 | elevation: 5, 73 | shadowOffset: { width: 0, height: 3 }, 74 | shadowRadius: 6, 75 | shadowOpacity: 0.2, 76 | shadowColor: '#000', 77 | }, 78 | large: { 79 | elevation: 10, 80 | shadowOffset: { width: 0, height: 6 }, 81 | shadowRadius: 12, 82 | shadowOpacity: 0.4, 83 | shadowColor: '#000', 84 | }, 85 | }[level]; 86 | }; 87 | 88 | export const flexCenter = ( 89 | value?: Stitches.PropertyValue<'flexDirection'> 90 | ) => ({ 91 | flexDirection: value || 'column', 92 | justifyContent: 'center', 93 | alignItems: 'center', 94 | }); 95 | 96 | export const absoluteFill = () => ({ 97 | ...StyleSheet.absoluteFillObject, 98 | }); 99 | 100 | export const generateSameMediaProperty = < 101 | Property extends keyof CSSProperties, 102 | Value 103 | >( 104 | property: Property, 105 | value: Value 106 | ) => { 107 | return { 108 | '@xl': { 109 | [property]: value, 110 | }, 111 | '@lg': { 112 | [property]: value, 113 | }, 114 | '@md': { 115 | [property]: value, 116 | }, 117 | '@sm': { 118 | [property]: value, 119 | }, 120 | '@xsm': { 121 | [property]: value, 122 | }, 123 | '@xxsm': { 124 | [property]: value, 125 | }, 126 | }; 127 | }; 128 | 129 | const fontSizes = { 130 | xxs: 10, 131 | xs: 14, 132 | sm: 16, 133 | md: 18, 134 | lg: 20, 135 | xl: 24, 136 | xxl: 32, 137 | }; 138 | 139 | export const remFunction = 140 | (property: Property) => 141 | (rValue: number) => { 142 | return { 143 | '@xxl': { 144 | [property]: fontSizes.xxl * rValue, 145 | }, 146 | '@xl': { 147 | [property]: fontSizes.xl * rValue, 148 | }, 149 | '@lg': { 150 | [property]: fontSizes.lg * rValue, 151 | }, 152 | '@md': { 153 | [property]: fontSizes.md * rValue, 154 | }, 155 | '@sm': { 156 | [property]: fontSizes.sm * rValue, 157 | }, 158 | '@xs': { 159 | [property]: fontSizes.xs * rValue, 160 | }, 161 | '@xxs': { 162 | [property]: fontSizes.xs * rValue, 163 | }, 164 | } as Record<`@${keyof typeof fontSizes}`, CSSProperties>; 165 | }; 166 | -------------------------------------------------------------------------------- /example/src/type-test.tsx: -------------------------------------------------------------------------------- 1 | import { styled, css } from './styles'; 2 | 3 | const View = styled('View', {}); 4 | 5 | const csstest = css({ 6 | fontSize: 16, 7 | }); 8 | 9 | export const Test = ; 10 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "stitches-native": ["../src/types/index.d.ts"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const createExpoWebpackConfigAsync = require('@expo/webpack-config'); 3 | const path = require('path'); 4 | 5 | module.exports = async function (env, argv) { 6 | const config = await createExpoWebpackConfigAsync(env, argv); 7 | 8 | Object.assign(config.resolve.alias, { 9 | react: path.join(__dirname, 'node_modules', 'react'), 10 | 'react-native': path.join(__dirname, 'node_modules', 'react-native-web'), 11 | 'react-native-web': path.join(__dirname, 'node_modules', 'react-native-web'), // prettier-ignore 12 | }); 13 | 14 | return config; 15 | }; 16 | -------------------------------------------------------------------------------- /media/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Temzasse/stitches-native/bd795a8d62007aa505463fdaa8e0d5258f668026/media/logo.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stitches-native", 3 | "description": "The modern CSS-in-JS library for React Native", 4 | "version": "0.4.0", 5 | "license": "MIT", 6 | "author": "Teemu Taskula", 7 | "repository": "https://github.com/Temzasse/stitches-native", 8 | "main": "lib/commonjs/index.js", 9 | "module": "lib/module/index.js", 10 | "types": "src/types/index.d.ts", 11 | "typesVersions": { 12 | ">= 4.1": { 13 | "*": [ 14 | "types/index.d.ts" 15 | ] 16 | } 17 | }, 18 | "exports": { 19 | "./package.json": "./package.json", 20 | ".": { 21 | "require": "./lib/commonjs/index.js", 22 | "import": "./lib/module/index.js", 23 | "types": "./src/types/index.d.ts" 24 | } 25 | }, 26 | "files": [ 27 | "src/types", 28 | "lib" 29 | ], 30 | "engines": { 31 | "node": ">=12" 32 | }, 33 | "scripts": { 34 | "build": "bob build", 35 | "prepare": "yarn build", 36 | "release": "np --any-branch", 37 | "watch": "npm-watch", 38 | "test": "jest", 39 | "lint": "yarn lint:lib & yarn lint:example", 40 | "lint:lib": "eslint ./src --ext .js --config .eslintrc", 41 | "lint:lib:fix": "eslint ./src --ext .js --config .eslintrc --fix", 42 | "lint:example": "eslint ./example --ext .ts,.tsx --config .eslintrc", 43 | "lint:example:fix": "eslint ./example --ext .ts,.tsx --config .eslintrc --fix", 44 | "format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\"", 45 | "format:write": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"" 46 | }, 47 | "dependencies": { 48 | "lodash.merge": "4.6.2" 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "7.14.8", 52 | "@babel/runtime": "7.14.8", 53 | "@testing-library/jest-native": "4.0.1", 54 | "@testing-library/react-native": "7.2.0", 55 | "@types/jest": "26.0.24", 56 | "@types/react": "^18.0.24", 57 | "@types/react-native": "^0.70.6", 58 | "@typescript-eslint/eslint-plugin": "4.6.1", 59 | "@typescript-eslint/parser": "4.6.1", 60 | "babel-jest": "27.0.6", 61 | "eslint": "7.31.0", 62 | "eslint-config-prettier": "8.3.0", 63 | "eslint-config-standard": "16.0.3", 64 | "eslint-plugin-import": "2.23.4", 65 | "eslint-plugin-node": "11.1.0", 66 | "eslint-plugin-prettier": "3.4.0", 67 | "eslint-plugin-promise": "5.1.0", 68 | "eslint-plugin-react": "7.24.0", 69 | "eslint-plugin-react-hooks": "4.2.0", 70 | "eslint-plugin-standard": "5.0.0", 71 | "husky": "6.0.0", 72 | "jest": "27.0.6", 73 | "metro-react-native-babel-preset": "^0.73.3", 74 | "npm-watch": "^0.11.0", 75 | "prettier": "2.3.2", 76 | "react": "18.0.0", 77 | "react-native": "0.69.6", 78 | "react-native-builder-bob": "0.18.1", 79 | "react-test-renderer": "18.0.0", 80 | "typescript": "4.7.3" 81 | }, 82 | "peerDependencies": { 83 | "react": ">=16", 84 | "react-native": "*" 85 | }, 86 | "husky": { 87 | "hooks": { 88 | "pre-commit": "yarn lint" 89 | } 90 | }, 91 | "jest": { 92 | "preset": "react-native", 93 | "modulePathIgnorePatterns": [ 94 | "/example/node_modules", 95 | "/lib/" 96 | ], 97 | "setupFilesAfterEnv": [ 98 | "@testing-library/jest-native/extend-expect" 99 | ] 100 | }, 101 | "react-native-builder-bob": { 102 | "source": "src/internals", 103 | "output": "lib", 104 | "targets": [ 105 | "commonjs", 106 | "module" 107 | ] 108 | }, 109 | "watch": { 110 | "build": { 111 | "patterns": [ 112 | "src" 113 | ], 114 | "extensions": "js,jsx,ts,tsx" 115 | } 116 | }, 117 | "eslintIgnore": [ 118 | "node_modules/", 119 | "lib/" 120 | ], 121 | "prettier": { 122 | "printWidth": 80, 123 | "semi": true, 124 | "singleQuote": true, 125 | "tabWidth": 2, 126 | "trailingComma": "es5" 127 | }, 128 | "np": { 129 | "yarn": true 130 | }, 131 | "keywords": [ 132 | "android", 133 | "ios", 134 | "react", 135 | "react-native", 136 | "native", 137 | "component", 138 | "components", 139 | "create", 140 | "css", 141 | "css-in-js", 142 | "javascript", 143 | "js", 144 | "object", 145 | "object-oriented", 146 | "oo", 147 | "oocss", 148 | "oriented", 149 | "style", 150 | "styled", 151 | "styles", 152 | "stylesheet", 153 | "stylesheets", 154 | "theme", 155 | "themes", 156 | "theming", 157 | "token", 158 | "tokens", 159 | "type", 160 | "typed", 161 | "types", 162 | "ts", 163 | "jsx", 164 | "tsx" 165 | ] 166 | } 167 | -------------------------------------------------------------------------------- /src/internals/constants.js: -------------------------------------------------------------------------------- 1 | export const COLOR_PROPERTIES = { 2 | backgroundColor: 'colors', 3 | border: 'colors', 4 | borderBottomColor: 'colors', 5 | borderColor: 'colors', 6 | borderEndColor: 'colors', 7 | borderLeftColor: 'colors', 8 | borderRightColor: 'colors', 9 | borderStartColor: 'colors', 10 | borderTopColor: 'colors', 11 | color: 'colors', 12 | overlayColor: 'colors', 13 | shadowColor: 'colors', 14 | textDecoration: 'colors', 15 | textShadowColor: 'colors', 16 | tintColor: 'colors', 17 | }; 18 | 19 | export const RADII_PROPERTIES = { 20 | borderBottomLeftRadius: 'radii', 21 | borderBottomRightRadius: 'radii', 22 | borderBottomStartRadius: 'radii', 23 | borderBottomEndRadius: 'radii', 24 | borderRadius: 'radii', 25 | borderTopLeftRadius: 'radii', 26 | borderTopRightRadius: 'radii', 27 | borderTopStartRadius: 'radii', 28 | borderTopEndRadius: 'radii', 29 | }; 30 | 31 | export const SPACE_PROPERTIES = { 32 | bottom: 'space', 33 | left: 'space', 34 | margin: 'space', 35 | marginBottom: 'space', 36 | marginEnd: 'space', 37 | marginHorizontal: 'space', 38 | marginLeft: 'space', 39 | marginRight: 'space', 40 | marginStart: 'space', 41 | marginTop: 'space', 42 | marginVertical: 'space', 43 | padding: 'space', 44 | paddingBottom: 'space', 45 | paddingEnd: 'space', 46 | paddingHorizontal: 'space', 47 | paddingLeft: 'space', 48 | paddingRight: 'space', 49 | paddingStart: 'space', 50 | paddingTop: 'space', 51 | paddingVertical: 'space', 52 | right: 'space', 53 | top: 'space', 54 | }; 55 | 56 | export const SIZE_PROPERTIES = { 57 | flexBasis: 'sizes', 58 | height: 'sizes', 59 | maxHeight: 'sizes', 60 | maxWidth: 'sizes', 61 | minHeight: 'sizes', 62 | minWidth: 'sizes', 63 | width: 'sizes', 64 | }; 65 | 66 | export const FONT_PROPERTIES = { 67 | fontFamily: 'fonts', 68 | }; 69 | 70 | export const FONT_SIZE_PROPERTIES = { 71 | fontSize: 'fontSizes', 72 | }; 73 | 74 | export const FONT_WEIGHT_PROPERTIES = { 75 | fontWeight: 'fontWeights', 76 | }; 77 | 78 | export const LINE_HEIGHT_PROPERTIES = { 79 | lineHeight: 'lineHeights', 80 | }; 81 | 82 | export const LETTER_SPACING_PROPERTIES = { 83 | letterSpacing: 'letterSpacings', 84 | }; 85 | 86 | export const Z_INDEX_PROPERTIES = { 87 | zIndex: 'zIndices', 88 | }; 89 | 90 | export const BORDER_WIDTH_PROPERTIES = { 91 | borderWidth: 'borderWidths', 92 | borderTopWidth: 'borderWidths', 93 | borderRightWidth: 'borderWidths', 94 | borderBottomWidth: 'borderWidths', 95 | borderLeftWidth: 'borderWidths', 96 | borderEndWidth: 'borderWidths', 97 | borderStartWidth: 'borderWidths', 98 | }; 99 | 100 | export const BORDER_STYLE_PROPERTIES = { 101 | borderStyle: 'borderStyles', 102 | }; 103 | 104 | export const DEFAULT_THEME_MAP = { 105 | ...BORDER_STYLE_PROPERTIES, 106 | ...BORDER_WIDTH_PROPERTIES, 107 | ...COLOR_PROPERTIES, 108 | ...FONT_PROPERTIES, 109 | ...FONT_SIZE_PROPERTIES, 110 | ...FONT_WEIGHT_PROPERTIES, 111 | ...LETTER_SPACING_PROPERTIES, 112 | ...LINE_HEIGHT_PROPERTIES, 113 | ...RADII_PROPERTIES, 114 | ...SIZE_PROPERTIES, 115 | ...SPACE_PROPERTIES, 116 | ...Z_INDEX_PROPERTIES, 117 | }; 118 | 119 | export const THEME_VALUES = { 120 | borderStyles: null, 121 | borderWidths: null, 122 | colors: null, 123 | fonts: null, 124 | fontSizes: null, 125 | fontWeights: null, 126 | letterSpacings: null, 127 | lineHeights: null, 128 | radii: null, 129 | sizes: null, 130 | space: null, 131 | zIndices: null, 132 | }; 133 | 134 | export const EMPTY_THEME = { 135 | definition: { 136 | __ID__: 'theme-0', 137 | ...THEME_VALUES, 138 | }, 139 | values: { 140 | ...THEME_VALUES, 141 | }, 142 | }; 143 | 144 | export const THEME_PROVIDER_MISSING_MESSAGE = 145 | 'Your app should have a ThemeProvider in order to access the theme'; 146 | -------------------------------------------------------------------------------- /src/internals/index.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash.merge'; 2 | import { PixelRatio, useWindowDimensions } from 'react-native'; 3 | 4 | import React, { 5 | createContext, 6 | createElement, 7 | forwardRef, 8 | memo, 9 | useMemo, 10 | useContext, 11 | } from 'react'; 12 | 13 | import * as utils from './utils'; 14 | import * as constants from './constants'; 15 | 16 | /** @typedef {import('../types').__Stitches__} Stitches */ 17 | /** @typedef {import('../types').CreateStitches} CreateStitches */ 18 | 19 | // eslint-disable-next-line 20 | const ReactNative = require('react-native'); 21 | 22 | /** @type {CreateStitches} */ 23 | export function createStitches(config = {}) { 24 | const themes = []; 25 | 26 | if (config.theme) { 27 | const processedTheme = utils.processTheme(config.theme); 28 | processedTheme.definition.__ID__ = 'theme-1'; 29 | 30 | themes.push(processedTheme); 31 | } else { 32 | themes.push(constants.EMPTY_THEME); 33 | } 34 | 35 | /** @type {Stitches['createTheme']} */ 36 | function createTheme(theme) { 37 | const newTheme = utils.processTheme( 38 | Object.entries(config.theme || {}).reduce((acc, [key, val]) => { 39 | acc[key] = { ...val, ...theme[key] }; 40 | return acc; 41 | }, {}) 42 | ); 43 | 44 | newTheme.definition.__ID__ = `theme-${themes.length + 1}`; 45 | 46 | themes.push(newTheme); 47 | 48 | return newTheme.definition; 49 | } 50 | 51 | const defaultTheme = themes[0].definition; 52 | const ThemeContext = createContext(defaultTheme); 53 | 54 | /** @type {Stitches['ThemeProvider']} */ 55 | function ThemeProvider({ theme = defaultTheme, children }) { 56 | return ( 57 | {children} 58 | ); 59 | } 60 | 61 | function useThemeInternal() { 62 | const themeDefinition = useContext(ThemeContext); 63 | 64 | if (!themeDefinition) { 65 | throw new Error(constants.THEME_PROVIDER_MISSING_MESSAGE); 66 | } 67 | 68 | return themes.find((x) => x.definition.__ID__ === themeDefinition.__ID__); 69 | } 70 | 71 | /** @type {Stitches['useTheme']} */ 72 | function useTheme() { 73 | const themeDefinition = useContext(ThemeContext); 74 | 75 | if (!themeDefinition) { 76 | throw new Error(constants.THEME_PROVIDER_MISSING_MESSAGE); 77 | } 78 | 79 | return themes.find((x) => x.definition.__ID__ === themeDefinition.__ID__).values; // prettier-ignore 80 | } 81 | 82 | /** @type {Stitches['styled']} */ 83 | function styled(component, ...styleObjects) { 84 | const styleObject = styleObjects.reduce((a, v) => merge(a, v), {}); 85 | 86 | const { 87 | variants = {}, 88 | compoundVariants = [], 89 | defaultVariants = {}, 90 | ..._styles 91 | } = styleObject; 92 | 93 | const styles = _styles; 94 | 95 | const styleSheets = {}; 96 | 97 | let attrsFn; 98 | 99 | let Comp = forwardRef((props, ref) => { 100 | const theme = useThemeInternal(); 101 | 102 | const styleSheet = useMemo(() => { 103 | const _styleSheet = styleSheets[theme.definition.__ID__]; 104 | if (_styleSheet) { 105 | return _styleSheet; 106 | } 107 | styleSheets[theme.definition.__ID__] = utils.createStyleSheet({ 108 | styles, 109 | config, 110 | theme, 111 | variants, 112 | compoundVariants, 113 | }); 114 | return styleSheets[theme.definition.__ID__]; 115 | }, [theme]); 116 | 117 | const { width: windowWidth } = useWindowDimensions(); 118 | 119 | let variantStyles = []; 120 | let compoundVariantStyles = []; 121 | 122 | const { mediaKey, breakpoint } = useMemo(() => { 123 | if (typeof config.media === 'object') { 124 | const correctedWindowWidth = 125 | PixelRatio.getPixelSizeForLayoutSize(windowWidth); 126 | 127 | // TODO: how do we quarantee the order of breakpoint matches? 128 | // The order of the media key value pairs should be constant 129 | // but is that guaranteed? So if the keys are ordered from 130 | // smallest screen size to largest everything should work ok... 131 | const _mediaKey = utils.resolveMediaRangeQuery( 132 | config.media, 133 | correctedWindowWidth 134 | ); 135 | 136 | return { 137 | mediaKey: _mediaKey, 138 | breakpoint: _mediaKey && `@${_mediaKey}`, 139 | }; 140 | } 141 | 142 | return {}; 143 | }, [windowWidth]); 144 | 145 | if (variants) { 146 | variantStyles = Object.keys(variants) 147 | .map((prop) => { 148 | let propValue = props[prop]; 149 | 150 | if (propValue === undefined) { 151 | propValue = defaultVariants[prop]; 152 | } 153 | 154 | let styleSheetKey = `${prop}_${propValue}`; 155 | 156 | // Handle responsive prop value 157 | // NOTE: only one media query will be applied since the `styleSheetKey` 158 | // is being rewritten by the last matching media query and defaults to `@initial` 159 | if ( 160 | typeof propValue === 'object' && 161 | typeof config.media === 'object' 162 | ) { 163 | // `@initial` acts as the default value if none of the media query values match 164 | // It's basically the as setting `prop="value"`, eg. `color="primary"` 165 | if (typeof propValue['@initial'] === 'string') { 166 | styleSheetKey = `${prop}_${propValue['@initial']}`; 167 | } 168 | 169 | if (breakpoint && propValue[breakpoint] !== undefined) { 170 | const val = config.media[mediaKey]; 171 | 172 | if (val === true || typeof val === 'string') { 173 | styleSheetKey = `${prop}_${propValue[breakpoint]}`; 174 | } 175 | } 176 | } 177 | 178 | const extractedStyle = styleSheetKey 179 | ? styleSheet[styleSheetKey] 180 | : undefined; 181 | 182 | if (extractedStyle && breakpoint in extractedStyle) { 183 | // WARNING: lodash merge modifies the first argument reference or skips if object is frozen. 184 | return merge({}, extractedStyle, extractedStyle[breakpoint]); 185 | } 186 | 187 | return extractedStyle; 188 | }) 189 | .filter(Boolean); 190 | } 191 | 192 | if (compoundVariants) { 193 | compoundVariantStyles = compoundVariants 194 | .map((compoundVariant) => { 195 | // eslint-disable-next-line 196 | const { css: _css, ...compounds } = compoundVariant; 197 | const compoundEntries = Object.entries(compounds); 198 | 199 | if ( 200 | compoundEntries.every(([prop, value]) => { 201 | const propValue = props[prop] ?? defaultVariants[prop]; 202 | return propValue === value; 203 | }) 204 | ) { 205 | const key = utils.getCompoundKey(compoundEntries); 206 | const extractedStyle = styleSheet[key]; 207 | 208 | if (extractedStyle && breakpoint in extractedStyle) { 209 | // WARNING: lodash merge modifies the first argument reference or skips if object is frozen. 210 | return merge({}, extractedStyle, extractedStyle[breakpoint]); 211 | } 212 | 213 | return extractedStyle; 214 | } 215 | }) 216 | .filter(Boolean); 217 | } 218 | 219 | let cssStyles = props.css 220 | ? utils.processStyles({ 221 | styles: props.css || {}, 222 | theme: theme.values, 223 | config, 224 | }) 225 | : {}; 226 | 227 | if (cssStyles && breakpoint in cssStyles) { 228 | // WARNING: lodash merge modifies the first argument reference or skips if object is frozen. 229 | cssStyles = merge({}, cssStyles, cssStyles[breakpoint]); 230 | } 231 | 232 | const mediaStyle = styleSheet.base[breakpoint] || {}; 233 | 234 | const stitchesStyles = [ 235 | styleSheet.base, 236 | mediaStyle, 237 | ...variantStyles, 238 | ...compoundVariantStyles, 239 | cssStyles, 240 | ]; 241 | 242 | const allStyles = 243 | typeof props.style === 'function' 244 | ? (...rest) => 245 | [props.style(...rest), ...stitchesStyles].filter(Boolean) 246 | : [...stitchesStyles, props.style].filter(Boolean); 247 | 248 | let attrsProps = {}; 249 | 250 | if (typeof attrsFn === 'function') { 251 | attrsProps = attrsFn({ ...props, theme: theme.values }); 252 | } 253 | 254 | const propsWithoutVariant = { ...props }; 255 | 256 | for (const variantKey of Object.keys(variants)) { 257 | delete propsWithoutVariant[variantKey]; 258 | } 259 | 260 | const componentProps = { 261 | ...attrsProps, 262 | ...propsWithoutVariant, 263 | style: allStyles, 264 | ref, 265 | }; 266 | 267 | if (typeof component === 'string') { 268 | return createElement(ReactNative[component], componentProps); 269 | } else if ( 270 | typeof component === 'object' || 271 | typeof component === 'function' 272 | ) { 273 | return createElement(component, componentProps); 274 | } 275 | 276 | return null; 277 | }); 278 | 279 | Comp = memo(Comp); 280 | 281 | Comp.attrs = (cb) => { 282 | attrsFn = cb; 283 | return Comp; 284 | }; 285 | 286 | return Comp; 287 | } 288 | 289 | /** @type {Stitches['css']} */ 290 | function css(...cssObjects) { 291 | return cssObjects.reduce((a, v) => merge(a, v), {}); 292 | } 293 | 294 | return { 295 | styled, 296 | css, 297 | theme: themes[0].definition, 298 | createTheme, 299 | useTheme, 300 | ThemeProvider, 301 | config, 302 | media: config.media, 303 | utils: config.utils, 304 | }; 305 | } 306 | 307 | export const { styled, css } = createStitches(); 308 | 309 | export const defaultThemeMap = constants.DEFAULT_THEME_MAP; 310 | 311 | export default createStitches; 312 | -------------------------------------------------------------------------------- /src/internals/utils.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import merge from 'lodash.merge'; 3 | import { DEFAULT_THEME_MAP, THEME_VALUES } from './constants'; 4 | 5 | export function getCompoundKey(compoundEntries) { 6 | // Eg. `color_primary+size_small` 7 | return ( 8 | compoundEntries 9 | // Sort compound entries alphabetically 10 | .sort((a, b) => { 11 | if (a[0] < b[0]) return -1; 12 | if (a[0] > b[0]) return 1; 13 | return 0; 14 | }) 15 | .reduce((keyAcc, [prop, value]) => { 16 | return keyAcc + `${prop}_${value}+`; 17 | }, '') 18 | .slice(0, -1) 19 | ); // Remove last `+` character 20 | } 21 | 22 | const validSigns = ['<=', '<', '>=', '>']; 23 | 24 | function matchMediaRangeQuery(query, windowWidth) { 25 | const singleRangeRegex = /^\(width\s+([><=]+)\s+([0-9]+)px\)$/; 26 | const multiRangeRegex = /^\(([0-9]+)px\s([><=]+)\swidth\s+([><=]+)\s+([0-9]+)px\)$/; // prettier-ignore 27 | const singleRangeMatches = query.match(singleRangeRegex); 28 | const multiRangeMatches = query.match(multiRangeRegex); 29 | 30 | let result; 31 | 32 | if (multiRangeMatches && multiRangeMatches.length === 5) { 33 | const [, _width1, sign1, sign2, _width2] = multiRangeMatches; 34 | const width1 = parseInt(_width1, 10); 35 | const width2 = parseInt(_width2, 10); 36 | 37 | if (validSigns.includes(sign1) && validSigns.includes(sign2)) { 38 | result = eval( 39 | `${width1} ${sign1} ${windowWidth} && ${windowWidth} ${sign2} ${width2}` 40 | ); 41 | } 42 | } else if (singleRangeMatches && singleRangeMatches.length === 3) { 43 | const [, sign, _width] = singleRangeMatches; 44 | const width = parseInt(_width, 10); 45 | 46 | if (validSigns.includes(sign)) { 47 | result = eval(`${windowWidth} ${sign} ${width}`); 48 | } 49 | } 50 | 51 | if (result === undefined) return false; 52 | 53 | if (typeof result !== 'boolean') { 54 | console.warn( 55 | `Unexpected media query result. Expected a boolean but got ${result}. Please make sure your media query syntax is correct.` 56 | ); 57 | } 58 | 59 | return result; 60 | } 61 | 62 | export function resolveMediaRangeQuery(media, windowWidth) { 63 | const entries = Object.entries(media); 64 | let result; 65 | 66 | for (let i = 0; i < entries.length; i++) { 67 | const [breakpoint, queryOrFlag] = entries[i]; 68 | 69 | // TODO: handle boolean flag 70 | if (typeof queryOrFlag !== 'string') continue; 71 | 72 | const match = matchMediaRangeQuery(queryOrFlag, windowWidth); 73 | 74 | if (match) { 75 | result = breakpoint; 76 | } 77 | } 78 | 79 | return result; 80 | } 81 | 82 | export function processThemeMap(themeMap) { 83 | const definition = {}; 84 | 85 | Object.keys(themeMap).forEach((token) => { 86 | const scale = themeMap[token]; 87 | 88 | if (!definition[scale]) { 89 | definition[scale] = {}; 90 | } 91 | 92 | definition[scale][token] = scale; 93 | }); 94 | 95 | return definition; 96 | } 97 | 98 | export function processTheme(theme) { 99 | const definition = {}; 100 | const values = {}; 101 | 102 | Object.keys(theme).forEach((scale) => { 103 | if (!definition[scale]) definition[scale] = {}; 104 | if (!values[scale]) values[scale] = {}; 105 | 106 | Object.keys(theme[scale]).forEach((token) => { 107 | let value = theme[scale][token]; 108 | 109 | if (typeof value === 'string' && value.length > 1 && value[0] === '$') { 110 | value = theme[scale][value.replace('$', '')]; 111 | } 112 | 113 | values[scale][token] = value; 114 | 115 | definition[scale][token] = { 116 | token, 117 | scale, 118 | value, 119 | toString: () => `$${token}`, 120 | }; 121 | }); 122 | }); 123 | 124 | return { definition, values }; 125 | } 126 | 127 | function getThemeKey(theme, themeMap, key) { 128 | return Object.keys(THEME_VALUES).find((themeKey) => { 129 | return key in (themeMap[themeKey] || {}) && theme?.[themeKey]; 130 | }); 131 | } 132 | 133 | export function processStyles({ styles, theme, config }) { 134 | const { utils, themeMap: customThemeMap } = config; 135 | const themeMap = processThemeMap(customThemeMap || DEFAULT_THEME_MAP); 136 | 137 | return Object.entries(styles).reduce((acc, [key, val]) => { 138 | if (utils && key in utils) { 139 | // NOTE: Deep merge for media properties. 140 | acc = merge( 141 | acc, 142 | processStyles({ styles: utils[key](val), theme, config }) 143 | ); 144 | } else if (typeof val === 'string' && val.indexOf('$') !== -1) { 145 | // Handle theme tokens, eg. `color: "$primary"` or `color: "$colors$primary"` 146 | const arr = val.split('$'); 147 | const token = arr.pop(); 148 | const scaleOrSign = arr.pop(); 149 | const maybeSign = arr.pop(); // handle negative values 150 | const scale = scaleOrSign !== '-' ? scaleOrSign : undefined; 151 | const sign = scaleOrSign === '-' || maybeSign === '-' ? -1 : undefined; 152 | 153 | if (scale && theme[scale]) { 154 | acc[key] = theme[scale][token]; 155 | } else { 156 | const themeKey = getThemeKey(theme, themeMap, key); 157 | if (themeKey) { 158 | acc[key] = theme[themeKey][token]; 159 | } 160 | } 161 | if (typeof acc[key] === 'number' && sign) { 162 | acc[key] *= sign; 163 | } 164 | } else if (typeof val === 'object' && val.value !== undefined) { 165 | // Handle cases where the value comes from the `theme` returned by `createStitches` 166 | acc[key] = val.value; 167 | } else if (typeof acc[key] === 'object' && typeof val === 'object') { 168 | // Handle cases where media object value comes from top of a style prop and variants' ones 169 | acc[key] = merge(acc[key], val); 170 | } else { 171 | acc[key] = val; 172 | } 173 | 174 | return acc; 175 | }, {}); 176 | } 177 | 178 | export function createStyleSheet({ 179 | theme, 180 | styles, 181 | config, 182 | variants, 183 | compoundVariants, 184 | }) { 185 | return StyleSheet.create({ 186 | base: styles ? processStyles({ styles, config, theme: theme.values }) : {}, 187 | // Variant styles 188 | ...Object.entries(variants).reduce( 189 | (variantsAcc, [variantProp, variantValues]) => { 190 | Object.entries(variantValues).forEach( 191 | ([variantName, variantStyles]) => { 192 | // Eg. `color_primary` or `size_small` 193 | const key = `${variantProp}_${variantName}`; 194 | 195 | variantsAcc[key] = processStyles({ 196 | styles: variantStyles, 197 | config, 198 | theme: theme.values, 199 | }); 200 | } 201 | ); 202 | return variantsAcc; 203 | }, 204 | {} 205 | ), 206 | // Compound variant styles 207 | ...compoundVariants.reduce((compoundAcc, compoundVariant) => { 208 | const { css, ...compounds } = compoundVariant; 209 | const compoundEntries = Object.entries(compounds); 210 | 211 | if (compoundEntries.length > 1) { 212 | const key = getCompoundKey(compoundEntries); 213 | 214 | compoundAcc[key] = processStyles({ 215 | styles: css || {}, 216 | config, 217 | theme: theme.values, 218 | }); 219 | } 220 | 221 | return compoundAcc; 222 | }, {}), 223 | }); 224 | } 225 | -------------------------------------------------------------------------------- /src/tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react-native'; 3 | import { createStitches as _createStitches } from '../internals'; 4 | import type { CreateStitches } from '../types'; 5 | 6 | // NOTE: the JSDoc types from the internal `createStitches` are not working properly 7 | const createStitches = _createStitches as CreateStitches; 8 | 9 | describe('Basic', () => { 10 | it('Functionality of styled()', () => { 11 | const { styled } = createStitches(); 12 | 13 | const Comp = styled('View', { 14 | backgroundColor: 'red', 15 | height: 100, 16 | width: 100, 17 | }); 18 | 19 | const { toJSON } = render(); 20 | const result = toJSON(); 21 | 22 | expect(result?.type).toEqual('View'); 23 | expect(result?.props.style[0]).toMatchObject({ 24 | backgroundColor: 'red', 25 | height: 100, 26 | width: 100, 27 | }); 28 | }); 29 | 30 | it('Functionality of styled() should not trigger recompute when a runtime theme is not used', () => { 31 | const { styled, createTheme } = createStitches({ 32 | theme: { 33 | sizes: { demoWidth: 100 }, 34 | }, 35 | }); 36 | 37 | const Comp = styled('View', { 38 | backgroundColor: 'red', 39 | height: 100, 40 | width: '$demoWidth', 41 | }); 42 | 43 | render(); 44 | 45 | createTheme({ sizes: { demoWidth: 10 } }); 46 | 47 | const { toJSON } = render(); 48 | 49 | const result = toJSON(); 50 | 51 | expect(result?.type).toEqual('View'); 52 | expect(result?.props.style[0]).toMatchObject({ 53 | backgroundColor: 'red', 54 | height: 100, 55 | width: 100, 56 | }); 57 | }); 58 | }); 59 | 60 | describe('Runtime', () => { 61 | it('Functionality of ThemeProvider', () => { 62 | const { styled, createTheme, ThemeProvider } = createStitches({ 63 | theme: { 64 | sizes: { demoWidth: 100 }, 65 | }, 66 | }); 67 | 68 | const Comp = styled('View', { 69 | backgroundColor: 'red', 70 | height: 100, 71 | width: '$demoWidth', 72 | }); 73 | 74 | const newTheme = createTheme({ sizes: { demoWidth: 30 } }); 75 | 76 | const { toJSON } = render( 77 | 78 | 79 | 80 | ); 81 | 82 | const result = toJSON(); 83 | 84 | expect(result?.type).toEqual('View'); 85 | expect(result?.props.style[0]).toMatchObject({ 86 | backgroundColor: 'red', 87 | height: 100, 88 | width: 30, 89 | }); 90 | }); 91 | 92 | it('Functionality of ThemeProvider should use new theme when a runtime theme is added', () => { 93 | const { styled, createTheme, ThemeProvider } = createStitches({ 94 | theme: { 95 | sizes: { demoWidth: 100 }, 96 | }, 97 | }); 98 | 99 | const Comp = styled('View', { 100 | backgroundColor: 'red', 101 | height: 100, 102 | width: '$demoWidth', 103 | }); 104 | 105 | const newTheme = createTheme({ sizes: { demoWidth: 10 } }); 106 | 107 | const { toJSON } = render( 108 | 109 | 110 | 111 | ); 112 | 113 | const result = toJSON(); 114 | 115 | expect(result?.type).toEqual('View'); 116 | expect(result?.props.style[0]).toMatchObject({ 117 | backgroundColor: 'red', 118 | height: 100, 119 | width: 10, 120 | }); 121 | }); 122 | 123 | it('Functionality of ThemeProvider should trigger recompute when a runtime theme is added', () => { 124 | const { styled, createTheme, ThemeProvider } = createStitches({ 125 | theme: { 126 | sizes: { demoWidth: 100 }, 127 | }, 128 | }); 129 | 130 | const Comp = styled('View', { 131 | backgroundColor: 'red', 132 | height: 100, 133 | width: '$demoWidth', 134 | }); 135 | 136 | render(); 137 | 138 | const newTheme = createTheme({ sizes: { demoWidth: 10 } }); 139 | 140 | const { toJSON } = render( 141 | 142 | 143 | 144 | ); 145 | 146 | const result = toJSON(); 147 | 148 | expect(result?.type).toEqual('View'); 149 | expect(result?.props.style[0]).toMatchObject({ 150 | backgroundColor: 'red', 151 | height: 100, 152 | width: 10, 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /src/types/config.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type * as CSSUtil from './css-util'; 3 | import type Stitches from './stitches'; 4 | 5 | /** Configuration Interface */ 6 | declare namespace ConfigType { 7 | /** Media interface. */ 8 | export type Media = { 9 | [name in keyof T]: T[name] extends string ? T[name] : string | boolean; 10 | }; 11 | 12 | /** Theme interface. */ 13 | export type Theme = { 14 | borderStyles?: { [token in number | string]: boolean | number | string }; 15 | borderWidths?: { [token in number | string]: boolean | number | string }; 16 | colors?: { [token in number | string]: boolean | number | string }; 17 | fonts?: { [token in number | string]: boolean | number | string }; 18 | fontSizes?: { [token in number | string]: boolean | number | string }; 19 | fontWeights?: { [token in number | string]: boolean | number | string }; 20 | letterSpacings?: { [token in number | string]: boolean | number | string }; 21 | lineHeights?: { [token in number | string]: boolean | number | string }; 22 | radii?: { [token in number | string]: boolean | number | string }; 23 | sizes?: { [token in number | string]: boolean | number | string }; 24 | space?: { [token in number | string]: boolean | number | string }; 25 | zIndices?: { [token in number | string]: boolean | number | string }; 26 | } & { 27 | [Scale in keyof T]: { 28 | [Token in keyof T[Scale]]: T[Scale][Token] extends 29 | | boolean 30 | | number 31 | | string 32 | ? T[Scale][Token] 33 | : boolean | number | string; 34 | }; 35 | }; 36 | 37 | /** ThemeMap interface. */ 38 | export type ThemeMap = { 39 | [Property in keyof T]: T[Property] extends string ? T[Property] : string; 40 | }; 41 | 42 | /** Utility interface. */ 43 | export type Utils = { 44 | [Property in keyof T]: T[Property] extends (value: infer V) => {} 45 | ? 46 | | T[Property] 47 | | ((value: V) => { 48 | [K in keyof CSSUtil.CSSProperties]?: CSSUtil.CSSProperties[K] | V; 49 | }) 50 | : never; 51 | }; 52 | } 53 | 54 | /** Default ThemeMap. */ 55 | export interface DefaultThemeMap { 56 | backgroundColor: 'colors'; 57 | border: 'colors'; 58 | borderBottomColor: 'colors'; 59 | borderColor: 'colors'; 60 | borderEndColor: 'colors'; 61 | borderLeftColor: 'colors'; 62 | borderRightColor: 'colors'; 63 | borderStartColor: 'colors'; 64 | borderTopColor: 'colors'; 65 | color: 'colors'; 66 | overlayColor: 'colors'; 67 | shadowColor: 'colors'; 68 | textDecoration: 'colors'; 69 | textShadowColor: 'colors'; 70 | tintColor: 'colors'; 71 | 72 | borderBottomLeftRadius: 'radii'; 73 | borderBottomRightRadius: 'radii'; 74 | borderBottomStartRadius: 'radii'; 75 | borderBottomEndRadius: 'radii'; 76 | borderRadius: 'radii'; 77 | borderTopLeftRadius: 'radii'; 78 | borderTopRightRadius: 'radii'; 79 | borderTopStartRadius: 'radii'; 80 | borderTopEndRadius: 'radii'; 81 | 82 | bottom: 'space'; 83 | left: 'space'; 84 | margin: 'space'; 85 | marginBottom: 'space'; 86 | marginEnd: 'space'; 87 | marginHorizontal: 'space'; 88 | marginLeft: 'space'; 89 | marginRight: 'space'; 90 | marginStart: 'space'; 91 | marginTop: 'space'; 92 | marginVertical: 'space'; 93 | padding: 'space'; 94 | paddingBottom: 'space'; 95 | paddingEnd: 'space'; 96 | paddingHorizontal: 'space'; 97 | paddingLeft: 'space'; 98 | paddingRight: 'space'; 99 | paddingStart: 'space'; 100 | paddingTop: 'space'; 101 | paddingVertical: 'space'; 102 | right: 'space'; 103 | top: 'space'; 104 | 105 | flexBasis: 'sizes'; 106 | height: 'sizes'; 107 | maxHeight: 'sizes'; 108 | maxWidth: 'sizes'; 109 | minHeight: 'sizes'; 110 | minWidth: 'sizes'; 111 | width: 'sizes'; 112 | 113 | fontFamily: 'fonts'; 114 | 115 | fontSize: 'fontSizes'; 116 | 117 | fontWeight: 'fontWeights'; 118 | 119 | lineHeight: 'lineHeights'; 120 | 121 | letterSpacing: 'letterSpacings'; 122 | 123 | zIndex: 'zIndices'; 124 | 125 | borderWidth: 'borderWidths'; 126 | borderTopWidth: 'borderWidths'; 127 | borderRightWidth: 'borderWidths'; 128 | borderBottomWidth: 'borderWidths'; 129 | borderLeftWidth: 'borderWidths'; 130 | borderStartWidth: 'borderWidths'; 131 | borderEndWidth: 'borderWidths'; 132 | 133 | borderStyle: 'borderStyles'; 134 | } 135 | 136 | /** Returns a function used to create a new Stitches interface. */ 137 | export type CreateStitches = { 138 | < 139 | Media extends {} = {}, 140 | Theme extends {} = {}, 141 | ThemeMap extends {} = DefaultThemeMap, 142 | Utils extends {} = {} 143 | >(config?: { 144 | media?: ConfigType.Media; 145 | theme?: ConfigType.Theme; 146 | themeMap?: ConfigType.ThemeMap; 147 | utils?: ConfigType.Utils; 148 | }): Stitches; 149 | }; 150 | -------------------------------------------------------------------------------- /src/types/css-util.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type * as Native from './react-native'; 3 | import type * as Config from './config'; 4 | import type * as Util from './util'; 5 | import type * as ThemeUtil from './theme'; 6 | 7 | export { Native }; 8 | 9 | /** CSS style declaration object. */ 10 | export interface CSSProperties extends Native.ReactNativeProperties {} 11 | 12 | type ValueByPropertyName = 13 | PropertyName extends keyof CSSProperties 14 | ? CSSProperties[PropertyName] 15 | : never; 16 | 17 | type TokenByPropertyName = 18 | PropertyName extends keyof ThemeMap 19 | ? TokenByScaleName 20 | : never; 21 | 22 | type TokenByScaleName = ScaleName extends keyof Theme 23 | ? Util.Prefixed<'$', keyof Theme[ScaleName]> 24 | : never; 25 | 26 | /** Returns a Style interface, leveraging the given media and style map. */ 27 | export type CSS< 28 | Media = {}, 29 | Theme = {}, 30 | ThemeMap = Config.DefaultThemeMap, 31 | Utils = {} 32 | > = { 33 | // nested at-rule css styles 34 | [K in Util.Prefixed<'@', keyof Media>]?: CSS; 35 | } & 36 | // known property styles 37 | { 38 | [K in keyof CSSProperties]?: 39 | | ValueByPropertyName 40 | | TokenByPropertyName 41 | | ThemeUtil.ScaleValue 42 | | undefined; 43 | } & 44 | // known utility styles 45 | { 46 | [K in keyof Utils as K extends keyof CSSProperties 47 | ? never 48 | : K]?: Utils[K] extends (arg: infer P) => any 49 | ? 50 | | (P extends any[] 51 | ? 52 | | ($$PropertyValue extends keyof P[0] 53 | ? 54 | | ValueByPropertyName 55 | | TokenByPropertyName< 56 | P[0][$$PropertyValue], 57 | Theme, 58 | ThemeMap 59 | > 60 | | ThemeUtil.ScaleValue 61 | | undefined 62 | : $$ScaleValue extends keyof P[0] 63 | ? 64 | | TokenByScaleName 65 | | { scale: P[0][$$ScaleValue] } 66 | | undefined 67 | : never)[] 68 | | P 69 | : $$PropertyValue extends keyof P 70 | ? 71 | | ValueByPropertyName 72 | | TokenByPropertyName 73 | | undefined 74 | : $$ScaleValue extends keyof P 75 | ? 76 | | TokenByScaleName 77 | | { scale: P[$$ScaleValue] } 78 | | undefined 79 | : never) 80 | | P 81 | : never; 82 | } & 83 | // known theme styles 84 | { 85 | [K in keyof ThemeMap as K extends keyof CSSProperties 86 | ? never 87 | : K extends keyof Utils 88 | ? never 89 | : K]?: Util.Index | undefined; 90 | } & { 91 | // unknown css declaration styles 92 | /** Unknown property. */ 93 | [K: string]: 94 | | number 95 | | string 96 | | CSS 97 | | {} 98 | | undefined; 99 | }; 100 | 101 | /** Unique symbol used to reference a property value. */ 102 | export declare const $$PropertyValue: unique symbol; 103 | 104 | /** Unique symbol used to reference a property value. */ 105 | export type $$PropertyValue = typeof $$PropertyValue; 106 | 107 | /** Unique symbol used to reference a token value. */ 108 | export declare const $$ScaleValue: unique symbol; 109 | 110 | /** Unique symbol used to reference a token value. */ 111 | export type $$ScaleValue = typeof $$ScaleValue; 112 | 113 | export declare const $$ThemeValue: unique symbol; 114 | 115 | export type $$ThemeValue = typeof $$ThemeValue; 116 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type Stitches from './stitches'; 3 | import type * as Config from './config'; 4 | import type * as CSSUtil from './css-util'; 5 | import type * as StyledComponent from './styled-component'; 6 | 7 | export type CreateStitches = Config.CreateStitches; 8 | export type CSSProperties = CSSUtil.CSSProperties; 9 | export type DefaultThemeMap = Config.DefaultThemeMap; 10 | export type __Stitches__ = Stitches; 11 | 12 | /** Returns a Style interface from a configuration, leveraging the given media and style map. */ 13 | 14 | export type CSS< 15 | Config extends { 16 | media?: {}; 17 | theme?: {}; 18 | themeMap?: {}; 19 | utils?: {}; 20 | } = { 21 | media: {}; 22 | theme: {}; 23 | themeMap: {}; 24 | utils: {}; 25 | } 26 | > = CSSUtil.CSS< 27 | Config['media'], 28 | Config['theme'], 29 | Config['themeMap'], 30 | Config['utils'] 31 | >; 32 | 33 | /** Returns the properties, attributes, and children expected by a component. */ 34 | export type ComponentProps = Component extends ( 35 | ...args: any[] 36 | ) => any 37 | ? Parameters[0] 38 | : never; 39 | 40 | /** Returns a type that expects a value to be a kind of CSS property value. */ 41 | export type PropertyValue = { 42 | readonly [CSSUtil.$$PropertyValue]: K; 43 | }; 44 | 45 | /** Returns a type that expects a value to be a kind of theme scale value. */ 46 | export type ScaleValue = { readonly [CSSUtil.$$ScaleValue]: K }; 47 | 48 | /** Returns a type that suggests variants from a component as possible prop values. */ 49 | export type VariantProps = 50 | StyledComponent.TransformProps< 51 | Component[StyledComponent.$$StyledComponentProps], 52 | Component[StyledComponent.$$StyledComponentMedia] 53 | >; 54 | 55 | /** Map of CSS properties to token scales. */ 56 | export declare const defaultThemeMap: DefaultThemeMap; 57 | 58 | /** Returns a library used to create styles. */ 59 | export declare const createStitches: CreateStitches; 60 | 61 | /** Returns an object representing a theme. */ 62 | export declare const createTheme: Stitches['createTheme']; 63 | 64 | /** Returns a function that applies styles and variants for a specific class. */ 65 | export declare const css: Stitches['css']; 66 | 67 | /** Returns a function that applies styles and variants for a specific class. */ 68 | export declare const styled: Stitches['styled']; 69 | -------------------------------------------------------------------------------- /src/types/react-native.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | ButtonProps, 5 | FlatListProps, 6 | ImageBackgroundProps, 7 | ImageProps, 8 | ImageStyle, 9 | InputAccessoryViewProps, 10 | KeyboardAvoidingViewProps, 11 | OpaqueColorValue, 12 | PressableProps, 13 | ScrollViewProps, 14 | SectionListProps, 15 | TextInputProps, 16 | TextProps, 17 | TextStyle, 18 | TouchableHighlightProps, 19 | TouchableNativeFeedbackProps, 20 | TouchableOpacityProps, 21 | TouchableWithoutFeedbackProps, 22 | ViewProps, 23 | ViewStyle, 24 | VirtualizedListProps, 25 | } from 'react-native'; 26 | 27 | type StringProperty = string & {}; 28 | type ColorProperty = OpaqueColorValue | (string & {}); 29 | type SimpleNumberProperty = number | (string & {}); 30 | type NumberProperty = TLength | number | (string & {}); 31 | // TODO: do we need both `SimpleNumberProperty` and `NumberProperty`? 32 | 33 | export interface ReactNativeProperties { 34 | alignContent: ViewStyle['alignContent']; 35 | alignItems: ViewStyle['alignItems']; 36 | alignSelf: ViewStyle['alignSelf']; 37 | aspectRatio: SimpleNumberProperty; 38 | backfaceVisibility: ViewStyle['backfaceVisibility']; 39 | backgroundColor: ColorProperty; 40 | borderBottomColor: ColorProperty; 41 | borderBottomEndRadius: SimpleNumberProperty; 42 | borderBottomLeftRadius: SimpleNumberProperty; 43 | borderBottomRightRadius: SimpleNumberProperty; 44 | borderBottomStartRadius: SimpleNumberProperty; 45 | borderBottomWidth: NumberProperty; 46 | borderColor: ColorProperty; 47 | borderEndColor: ColorProperty; 48 | borderEndWidth: NumberProperty; 49 | borderLeftColor: ColorProperty; 50 | borderLeftWidth: NumberProperty; 51 | borderRadius: SimpleNumberProperty; 52 | borderRightColor: ColorProperty; 53 | borderRightWidth: NumberProperty; 54 | borderStartColor: ColorProperty; 55 | borderStartWidth: NumberProperty; 56 | borderStyle: ViewStyle['borderStyle']; 57 | borderTopColor: ColorProperty; 58 | borderTopEndRadius: SimpleNumberProperty; 59 | borderTopLeftRadius: SimpleNumberProperty; 60 | borderTopRightRadius: SimpleNumberProperty; 61 | borderTopStartRadius: SimpleNumberProperty; 62 | borderTopWidth: NumberProperty; 63 | borderWidth: NumberProperty; 64 | bottom: NumberProperty; 65 | color: ColorProperty; 66 | direction: ViewStyle['direction']; 67 | display: ViewStyle['display']; 68 | elevation: NumberProperty; 69 | end: SimpleNumberProperty; 70 | flex: NumberProperty; 71 | flexBasis: NumberProperty; 72 | flexDirection: ViewStyle['flexDirection']; 73 | flexGrow: NumberProperty; 74 | flexShrink: NumberProperty; 75 | flexWrap: ViewStyle['flexWrap']; 76 | fontFamily: StringProperty; 77 | fontSize: NumberProperty; 78 | fontStyle: TextStyle['fontStyle']; 79 | fontVariant: TextStyle['fontVariant']; 80 | fontWeight: TextStyle['fontWeight']; 81 | height: NumberProperty; 82 | includeFontPadding: TextStyle['includeFontPadding']; 83 | justifyContent: ViewStyle['justifyContent']; 84 | left: NumberProperty; 85 | letterSpacing: NumberProperty; 86 | lineHeight: NumberProperty; 87 | margin: NumberProperty; 88 | marginBottom: NumberProperty; 89 | marginEnd: NumberProperty; 90 | marginHorizontal: NumberProperty; 91 | marginLeft: NumberProperty; 92 | marginRight: NumberProperty; 93 | marginStart: NumberProperty; 94 | marginTop: NumberProperty; 95 | marginVertical: NumberProperty; 96 | maxHeight: NumberProperty; 97 | maxWidth: NumberProperty; 98 | minHeight: NumberProperty; 99 | minWidth: NumberProperty; 100 | opacity: SimpleNumberProperty; 101 | overflow: ViewStyle['overflow']; 102 | overlayColor: ColorProperty; 103 | padding: NumberProperty; 104 | paddingBottom: NumberProperty; 105 | paddingEnd: NumberProperty; 106 | paddingHorizontal: NumberProperty; 107 | paddingLeft: NumberProperty; 108 | paddingRight: NumberProperty; 109 | paddingStart: NumberProperty; 110 | paddingTop: NumberProperty; 111 | paddingVertical: NumberProperty; 112 | position: ViewStyle['position']; 113 | resizeMode: ImageStyle['resizeMode']; 114 | right: NumberProperty; 115 | shadowColor: ColorProperty; 116 | shadowOffset: ViewStyle['shadowOffset']; 117 | shadowOpacity: SimpleNumberProperty; 118 | shadowRadius: SimpleNumberProperty; 119 | start: NumberProperty; 120 | textAlign: TextStyle['textAlign']; 121 | textDecorationColor: ColorProperty; 122 | textDecorationLine: TextStyle['textDecorationLine']; 123 | textDecorationStyle: TextStyle['textDecorationStyle']; 124 | textShadowColor: ColorProperty; 125 | textShadowOffset: TextStyle['textShadowOffset']; 126 | textShadowRadius: SimpleNumberProperty; 127 | textTransform: TextStyle['textTransform']; 128 | tintColor: ColorProperty; 129 | top: NumberProperty; 130 | width: NumberProperty; 131 | writingDirection: TextStyle['writingDirection']; 132 | zIndex: SimpleNumberProperty; 133 | } 134 | 135 | type PropsWithChildren = T & { children?: React.ReactNode }; 136 | 137 | export type ReactNativeElements = { 138 | Button: PropsWithChildren; 139 | FlatList: FlatListProps; 140 | Image: PropsWithChildren; 141 | ImageBackground: PropsWithChildren; 142 | InputAccessoryView: PropsWithChildren; 143 | KeyboardAvoidingView: PropsWithChildren; 144 | Pressable: PropsWithChildren; 145 | SafeAreaView: PropsWithChildren; 146 | ScrollView: PropsWithChildren; 147 | SectionList: SectionListProps; 148 | Text: PropsWithChildren; 149 | TextInput: PropsWithChildren; 150 | TouchableHighlight: PropsWithChildren; 151 | TouchableNativeFeedback: PropsWithChildren; 152 | TouchableOpacity: PropsWithChildren; 153 | TouchableWithoutFeedback: PropsWithChildren; 154 | View: PropsWithChildren; 155 | VirtualizedList: VirtualizedListProps; 156 | }; 157 | 158 | export type ReactNativeElementsKeys = keyof ReactNativeElements; 159 | 160 | // prettier-ignore 161 | export type ReactNativeElementType

= 162 | | { [K in ReactNativeElementsKeys]: P extends ReactNativeElements[K] ? K : never }[ReactNativeElementsKeys] 163 | | React.ComponentType

; 164 | 165 | type ReactNativeComponentProps< 166 | T extends ReactNativeElementsKeys | React.JSXElementConstructor 167 | > = T extends React.JSXElementConstructor 168 | ? P 169 | : T extends ReactNativeElementsKeys 170 | ? ReactNativeElements[T] 171 | : {}; 172 | 173 | export type ReactNativeComponentPropsWithRef = 174 | T extends React.ComponentClass 175 | ? React.PropsWithoutRef

& React.RefAttributes> 176 | : React.PropsWithRef>; 177 | -------------------------------------------------------------------------------- /src/types/stitches.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type * as React from 'react'; 3 | import type * as CSSUtil from './css-util'; 4 | import type * as StyledComponent from './styled-component'; 5 | import type * as Native from './react-native'; 6 | import type * as Util from './util'; 7 | import type * as ThemeUtil from './theme'; 8 | 9 | /** Stitches interface. */ 10 | export default interface Stitches< 11 | Media extends {} = {}, 12 | Theme extends {} = {}, 13 | ThemeMap extends {} = {}, 14 | Utils extends {} = {} 15 | > { 16 | config: { 17 | media: Media; 18 | theme: Theme; 19 | themeMap: ThemeMap; 20 | utils: Utils; 21 | }; 22 | createTheme: { 23 | < 24 | Arg extends { 25 | [Scale in keyof Theme]?: { 26 | [Token in keyof Theme[Scale]]?: boolean | number | string; 27 | }; 28 | } & 29 | { 30 | [scale in string]: { 31 | [token in number | string]: boolean | number | string; 32 | }; 33 | } 34 | >( 35 | arg: Arg 36 | ): string & ThemeTokens; 37 | }; 38 | theme: string & 39 | { 40 | [Scale in keyof Theme]: { 41 | [Token in keyof Theme[Scale]]: ThemeUtil.Token< 42 | Extract, 43 | string, 44 | Extract 45 | >; 46 | }; 47 | }; 48 | useTheme: () => { 49 | [Scale in keyof Theme]: { 50 | [Token in keyof Theme[Scale]]: Theme[Scale][Token] extends string 51 | ? ThemeUtil.AliasedToken extends never 52 | ? string 53 | : Theme[Scale][ThemeUtil.AliasedToken] 54 | : Theme[Scale][Token]; 55 | }; 56 | }; 57 | ThemeProvider: React.FunctionComponent<{ 58 | theme?: any; 59 | children: React.ReactNode; 60 | }>; // TODO: fix `any` 61 | css: { 62 | < 63 | Composers extends ( 64 | | string 65 | | React.ExoticComponent 66 | | React.JSXElementConstructor 67 | | Util.Function 68 | | { [name: string]: unknown } 69 | )[], 70 | CSS = CSSUtil.CSS 71 | >( 72 | ...composers: { 73 | [K in keyof Composers]: string extends Composers[K] // Strings, React Components, and Functions can be skipped over 74 | ? Composers[K] 75 | : Composers[K] extends 76 | | string 77 | | React.ExoticComponent 78 | | React.JSXElementConstructor 79 | | Util.Function 80 | ? Composers[K] 81 | : CSS & { 82 | /** The **variants** property lets you set a subclass of styles based on a key-value pair. 83 | * 84 | * [Read Documentation](https://stitches.dev/docs/variants) 85 | */ 86 | variants?: { 87 | [Name in string]: { 88 | [Pair in number | string]: CSS; 89 | }; 90 | }; 91 | /** The **compoundVariants** property lets you to set a subclass of styles based on a combination of active variants. 92 | * 93 | * [Read Documentation](https://stitches.dev/docs/variants#compound-variants) 94 | */ 95 | compoundVariants?: (('variants' extends keyof Composers[K] 96 | ? { 97 | [Name in keyof Composers[K]['variants']]?: 98 | | Util.Widen 99 | | Util.String; 100 | } & 101 | Util.WideObject 102 | : Util.WideObject) & { 103 | css: CSS; 104 | })[]; 105 | /** The **defaultVariants** property allows you to predefine the active key-value pairs of variants. 106 | * 107 | * [Read Documentation](https://stitches.dev/docs/variants#default-variants) 108 | */ 109 | defaultVariants?: 'variants' extends keyof Composers[K] 110 | ? { 111 | [Name in keyof Composers[K]['variants']]?: 112 | | Util.Widen 113 | | Util.String; 114 | } 115 | : Util.WideObject; 116 | } & { 117 | [K2 in keyof Composers[K]]: K2 extends 118 | | 'compoundVariants' 119 | | 'defaultVariants' 120 | | 'variants' 121 | ? unknown 122 | : K2 extends keyof CSS 123 | ? CSS[K2] 124 | : unknown; 125 | }; 126 | } 127 | ): CSS; 128 | }; // TODO: `variants` inside `css` break TS... 129 | styled: { 130 | < 131 | Type extends 132 | | Native.ReactNativeElementsKeys 133 | | React.ComponentType 134 | | Util.Function, 135 | Composers extends ( 136 | | string 137 | | React.ComponentType 138 | | Util.Function 139 | | { [name: string]: unknown } 140 | )[], 141 | CSS = CSSUtil.CSS 142 | >( 143 | type: Type, 144 | ...composers: { 145 | [K in keyof Composers]: string extends Composers[K] // Strings and Functions can be skipped over 146 | ? Composers[K] 147 | : Composers[K] extends 148 | | string 149 | | React.ComponentType 150 | | Util.Function 151 | ? Composers[K] 152 | : CSS & { 153 | /** The **variants** property lets you set a subclass of styles based on a key-value pair. 154 | * 155 | * [Read Documentation](https://stitches.dev/docs/variants) 156 | */ 157 | variants?: { 158 | [Name in string]: { 159 | [Pair in number | string]: CSS; 160 | }; 161 | }; 162 | /** The **compoundVariants** property lets you to set a subclass of styles based on a combination of active variants. 163 | * 164 | * [Read Documentation](https://stitches.dev/docs/variants#compound-variants) 165 | */ 166 | compoundVariants?: (('variants' extends keyof Composers[K] 167 | ? { 168 | [Name in keyof Composers[K]['variants']]?: 169 | | Util.Widen 170 | | Util.String; 171 | } & 172 | Util.WideObject 173 | : Util.WideObject) & { 174 | css: CSS; 175 | })[]; 176 | /** The **defaultVariants** property allows you to predefine the active key-value pairs of variants. 177 | * 178 | * [Read Documentation](https://stitches.dev/docs/variants#default-variants) 179 | */ 180 | defaultVariants?: 'variants' extends keyof Composers[K] 181 | ? { 182 | [Name in keyof Composers[K]['variants']]?: 183 | | Util.Widen 184 | | Util.String; 185 | } 186 | : Util.WideObject; 187 | } & { 188 | [K2 in keyof Composers[K]]: K2 extends 189 | | 'compoundVariants' 190 | | 'defaultVariants' 191 | | 'variants' 192 | ? unknown 193 | : K2 extends keyof CSS 194 | ? CSS[K2] 195 | : unknown; 196 | }; 197 | } 198 | ): StyledComponent.StyledComponent< 199 | Type, 200 | StyledComponent.StyledComponentProps, 201 | Media, 202 | CSSUtil.CSS 203 | > & { 204 | attrs: ( 205 | cb: ( 206 | props: { 207 | theme: Theme; 208 | } & StyledComponent.StyledComponentProps 209 | ) => Type extends 210 | | Native.ReactNativeElementsKeys 211 | | React.ComponentType 212 | ? Partial> 213 | : {} 214 | ) => StyledComponent.StyledComponent< 215 | Type, 216 | StyledComponent.StyledComponentProps, 217 | Media, 218 | CSSUtil.CSS 219 | >; 220 | }; 221 | }; 222 | } 223 | 224 | type ThemeTokens = { 225 | [Scale in keyof Values]: { 226 | [Token in keyof Values[Scale]]: ThemeUtil.Token< 227 | Extract, 228 | Values[Scale][Token], 229 | Extract 230 | >; 231 | }; 232 | }; 233 | -------------------------------------------------------------------------------- /src/types/styled-component.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type * as React from 'react'; 3 | import type * as Native from './react-native'; 4 | import type * as Util from './util'; 5 | 6 | /** Returns a new Styled Component. */ 7 | export interface StyledComponent< 8 | Type = 'View', 9 | Props = {}, 10 | Media = {}, 11 | CSS = {} 12 | > extends React.ForwardRefExoticComponent< 13 | Util.Assign< 14 | Type extends Native.ReactNativeElementsKeys | React.ComponentType 15 | ? Native.ReactNativeComponentPropsWithRef 16 | : never, 17 | TransformProps & { css?: CSS } 18 | > 19 | > { 20 | ( 21 | props: Util.Assign< 22 | Type extends Native.ReactNativeElementsKeys | React.ComponentType 23 | ? Native.ReactNativeComponentPropsWithRef 24 | : {}, 25 | TransformProps & { 26 | as?: never; 27 | css?: CSS; 28 | ref?: any; // TODO: remove this hack and fix the real `ref` type 29 | } 30 | > 31 | ): React.ReactElement | null; 32 | 33 | < 34 | C extends CSS, 35 | As extends string | React.ComponentType = Type extends 36 | | string 37 | | React.ComponentType 38 | ? Type 39 | : never 40 | >( 41 | props: Util.Assign< 42 | React.ComponentPropsWithRef< 43 | As extends Native.ReactNativeElementsKeys | React.ComponentType 44 | ? As 45 | : never 46 | >, 47 | TransformProps & { 48 | as?: As; 49 | css?: { 50 | [K in keyof C]: K extends keyof CSS ? CSS[K] : never; 51 | }; 52 | } 53 | > 54 | ): React.ReactElement | null; 55 | 56 | [$$StyledComponentType]: Type; 57 | [$$StyledComponentProps]: Props; 58 | [$$StyledComponentMedia]: Media; 59 | } 60 | 61 | /** Returns a new CSS Component. */ 62 | export interface CssComponent { 63 | ( 64 | props?: TransformProps & { 65 | css?: CSS; 66 | } & { 67 | [name in number | string]: any; 68 | } 69 | ): string & { 70 | props: {}; 71 | }; 72 | 73 | [$$StyledComponentType]: Type; 74 | [$$StyledComponentProps]: Props; 75 | [$$StyledComponentMedia]: Media; 76 | } 77 | 78 | export type TransformProps = { 79 | [K in keyof Props]: 80 | | Props[K] 81 | | ({ 82 | [KMedia in Util.Prefixed<'@', 'initial' | keyof Media>]?: Props[K]; 83 | } & 84 | { 85 | [KMedia in string]: Props[K]; 86 | }); 87 | }; 88 | 89 | /** Unique symbol used to reference the type of a Styled Component. */ 90 | export declare const $$StyledComponentType: unique symbol; 91 | 92 | /** Unique symbol used to reference the type of a Styled Component. */ 93 | export type $$StyledComponentType = typeof $$StyledComponentType; 94 | 95 | /** Unique symbol used to reference the props of a Styled Component. */ 96 | export declare const $$StyledComponentProps: unique symbol; 97 | 98 | /** Unique symbol used to reference the props of a Styled Component. */ 99 | export type $$StyledComponentProps = typeof $$StyledComponentProps; 100 | 101 | /** Unique symbol used to reference the media passed into a Styled Component. */ 102 | export declare const $$StyledComponentMedia: unique symbol; 103 | 104 | /** Unique symbol used to reference the media passed into a Styled Component. */ 105 | export type $$StyledComponentMedia = typeof $$StyledComponentMedia; 106 | 107 | /** Returns a narrowed JSX element from the given tag name. */ 108 | type IntrinsicElement = TagName extends Native.ReactNativeElementsKeys 109 | ? TagName 110 | : never; 111 | 112 | /** Returns a ForwardRef component. */ 113 | type ForwardRefExoticComponent = React.ForwardRefExoticComponent< 114 | Util.Assign< 115 | Type extends React.ElementType ? React.ComponentPropsWithRef : never, 116 | Props & { as?: Type } 117 | > 118 | >; 119 | 120 | /** Returns the first Styled Component type from the given array of compositions. */ 121 | export type StyledComponentType = T[0] extends never 122 | ? 'View' 123 | : T[0] extends string 124 | ? T[0] 125 | : T[0] extends (props: any) => any 126 | ? T[0] 127 | : T[0] extends { [$$StyledComponentType]: unknown } 128 | ? T[0][$$StyledComponentType] 129 | : T extends [lead: any, ...tail: infer V] 130 | ? StyledComponentType 131 | : never; 132 | 133 | /** Returns the cumulative variants from the given array of compositions. */ 134 | export type StyledComponentProps = 135 | ($$StyledComponentProps extends keyof T[0] 136 | ? T[0][$$StyledComponentProps] 137 | : T[0] extends { variants: { [name: string]: unknown } } 138 | ? { 139 | [K in keyof T[0]['variants']]?: Util.Widen; 140 | } 141 | : {}) & 142 | (T extends [lead: any, ...tail: infer V] ? StyledComponentProps : {}); 143 | -------------------------------------------------------------------------------- /src/types/theme.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export type AliasedToken = 4 | T extends `${infer Head}${infer Tail}` 5 | ? Head extends '$' 6 | ? Tail 7 | : never 8 | : never; 9 | 10 | export interface ScaleValue { 11 | token: number | string; 12 | value: number | string; 13 | scale: string; 14 | } 15 | 16 | export interface Token< 17 | /** Token name. */ 18 | NameType extends number | string = string, 19 | /** Token value. */ 20 | ValueType extends number | string = string, 21 | /** Token scale. */ 22 | ScaleType extends string | void = void 23 | > extends ScaleValue { 24 | new (name: NameType, value: ValueType, scale?: ScaleType): this; 25 | 26 | /** Name of the token. */ 27 | token: NameType; 28 | 29 | /** Value of the token. */ 30 | value: ValueType; 31 | 32 | /** Category of interface the token applies to. */ 33 | scale: ScaleType extends string ? ScaleType : ''; 34 | 35 | /** Returns variable prefixed with `$` representing the token. */ 36 | toString(): `$(${this['token']})`; 37 | } 38 | -------------------------------------------------------------------------------- /src/types/util.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | /* Utilities */ 4 | /* ========================================================================== */ 5 | 6 | /** Returns a string with the given prefix followed by the given values. */ 7 | export type Prefixed = `${K}${Extract< 8 | T, 9 | boolean | number | string 10 | >}`; 11 | 12 | /** Returns an object from the given object assigned with the values of another given object. */ 13 | export type Assign = Omit & T2; 14 | 15 | /** Returns a widened value from the given value. */ 16 | export type Widen = T extends number 17 | ? `${T}` | T 18 | : T extends 'true' 19 | ? boolean | T 20 | : T extends 'false' 21 | ? boolean | T 22 | : T extends `${number}` 23 | ? number | T 24 | : T; 25 | 26 | /** Narrowed string. */ 27 | export type String = string & Record; 28 | 29 | /** Narrowed number or string. */ 30 | export type Index = (number | string) & Record; 31 | 32 | /** Narrowed function. */ 33 | export type Function = (...args: any[]) => unknown; 34 | 35 | /** Widened object. */ 36 | export type WideObject = { 37 | [name in number | string]: boolean | number | string | undefined | WideObject; 38 | }; 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules", "example", "lib"], 3 | "include": ["src/index", "src/types"], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "allowUnreachableCode": false, 7 | "allowUnusedLabels": false, 8 | "baseUrl": ".", 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "importHelpers": true, 13 | "importsNotUsedAsValues": "error", 14 | "jsx": "react", 15 | "lib": ["esnext", "dom"], 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "noFallthroughCasesInSwitch": true, 19 | "noImplicitReturns": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "resolveJsonModule": true, 23 | "rootDir": "./src", 24 | "skipLibCheck": true, 25 | "sourceMap": true, 26 | "strict": true, 27 | "target": "esnext", 28 | "paths": { 29 | "stitches-native": ["./src/index"] 30 | } 31 | } 32 | } 33 | --------------------------------------------------------------------------------