├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml ├── release.yaml └── workflows │ └── ci.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── package.json ├── src ├── GlobalStyles.tsx ├── bin │ ├── shim_jsx_element.ts │ ├── tools │ │ ├── SemVer.ts │ │ ├── crawl.ts │ │ ├── fs.rmSync.ts │ │ └── transformCodebase.ts │ └── yarn_link.ts ├── compat.ts ├── cssAndCx.ts ├── dsfr.ts ├── index.ts ├── makeStyles.tsx ├── mergeClasses.ts ├── mui-compat.ts ├── mui │ ├── index.ts │ ├── mui.ts │ └── themeStyleOverridesPlugin.ts ├── next │ ├── appDir.tsx │ ├── index.ts │ └── pagesDir.tsx ├── nextJs.tsx ├── test │ ├── apps │ │ ├── next-appdir │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── app │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ ├── next-env.d.ts │ │ │ ├── next.config.js │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ ├── favicon.ico │ │ │ │ └── vercel.svg │ │ │ ├── shared │ │ │ │ ├── AppMuiThemeProvider.tsx │ │ │ │ └── tss-mui.ts │ │ │ ├── tsconfig.json │ │ │ └── yarn.lock │ │ ├── spa │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ ├── favicon.ico │ │ │ │ ├── index.html │ │ │ │ ├── logo192.png │ │ │ │ ├── logo512.png │ │ │ │ ├── manifest.json │ │ │ │ └── robots.txt │ │ │ ├── src │ │ │ │ ├── App.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── makeStyles.ts │ │ │ │ └── react-app-env.d.ts │ │ │ ├── tsconfig.json │ │ │ └── yarn.lock │ │ └── ssr │ │ │ ├── .eslintrc │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── next-env.d.ts │ │ │ ├── next.config.js │ │ │ ├── package.json │ │ │ ├── pages │ │ │ ├── _app.tsx │ │ │ ├── _document.tsx │ │ │ └── index.tsx │ │ │ ├── public │ │ │ ├── favicon.ico │ │ │ └── vercel.svg │ │ │ ├── shared │ │ │ ├── isDarkModeEnabled.tsx │ │ │ └── makeStyles.ts │ │ │ ├── tsconfig.json │ │ │ └── yarn.lock │ └── types │ │ ├── ReactComponent.tsx │ │ ├── makeStyles.tsx │ │ ├── mergeClasses.ts │ │ ├── tss.tsx │ │ ├── withStyle_htmlBuiltins.tsx │ │ ├── withStyles_className.tsx │ │ └── withStyles_classes.tsx ├── tools │ ├── ReactComponent.tsx │ ├── ReactHTML.tsx │ ├── assert.ts │ ├── capitalize.ts │ ├── classnames.ts │ ├── getDependencyArrayRef.ts │ ├── isSSR.ts │ ├── objectKeys.ts │ ├── polyfills │ │ └── Object.fromEntries.ts │ ├── typeGuard.ts │ └── useGuaranteedMemo.ts ├── tss.ts ├── types.ts ├── withStyles.tsx └── withStyles_compat.tsx ├── tsconfig-esm.json ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | CHANGELOG.md 4 | .yarn_home/ 5 | src/test/apps/ 6 | src/test/types/ 7 | src/tools/types/ 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint", 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier", 11 | ], 12 | "rules": { 13 | "no-extra-boolean-cast": "off", 14 | "@typescript-eslint/explicit-module-boundary-types": "off", 15 | "@typescript-eslint/no-explicit-any": "off", 16 | "@typescript-eslint/no-namespace": "off" 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/test/apps/**/* linguist-documentation 2 | src/tools/types/**/* linguist-documentation 3 | .eslintrc.js linguist-documentation 4 | next.config.js linguist-documentation 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [garronej] 4 | custom: ['https://www.ringerhq.com/experts/garronej'] 5 | -------------------------------------------------------------------------------- /.github/release.yaml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - octocat 7 | categories: 8 | - title: Breaking Changes 🛠 9 | labels: 10 | - breaking 11 | - title: Exciting New Features 🎉 12 | labels: 13 | - feature 14 | - title: Fixes 🔧 15 | labels: 16 | - fix 17 | - title: Documentation 🔧 18 | labels: 19 | - docs 20 | - title: CI 👷 21 | labels: 22 | - ci 23 | - title: Other Changes 24 | labels: 25 | - '*' -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | 12 | test_lint: 13 | runs-on: ubuntu-latest 14 | if: ${{ !github.event.created && github.repository != 'garronej/ts-ci' }} 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | - uses: bahmutov/npm-install@v1 19 | - name: If this step fails run 'npm run lint' and 'npm run format' then commit again. 20 | run: | 21 | PACKAGE_MANAGER=npm 22 | if [ -f "./yarn.lock" ]; then 23 | PACKAGE_MANAGER=yarn 24 | fi 25 | $PACKAGE_MANAGER run lint:check 26 | $PACKAGE_MANAGER run format:check 27 | test: 28 | runs-on: ${{ matrix.os }} 29 | needs: test_lint 30 | strategy: 31 | matrix: 32 | node: [ '17' ] 33 | os: [ ubuntu-latest ] 34 | name: Test with Node v${{ matrix.node }} on ${{ matrix.os }} 35 | steps: 36 | - name: Tell if project is using npm or yarn 37 | id: step1 38 | uses: garronej/ts-ci@v2.0.2 39 | with: 40 | action_name: tell_if_project_uses_npm_or_yarn 41 | - uses: actions/checkout@v3 42 | - uses: actions/setup-node@v3 43 | with: 44 | node-version: ${{ matrix.node }} 45 | - uses: bahmutov/npm-install@v1 46 | - if: steps.step1.outputs.npm_or_yarn == 'yarn' 47 | run: | 48 | yarn build 49 | - if: steps.step1.outputs.npm_or_yarn == 'npm' 50 | run: | 51 | npm run build 52 | check_if_version_upgraded: 53 | name: Check if version upgrade 54 | # We run this only if it's a push on the default branch or if it's a PR from a 55 | # branch (meaning not a PR from a fork). It would be more straightforward to test if secrets.NPM_TOKEN is 56 | # defined but GitHub Action don't allow it yet. 57 | if: | 58 | github.event_name == 'push' || 59 | github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login 60 | runs-on: ubuntu-latest 61 | needs: test 62 | outputs: 63 | from_version: ${{ steps.step1.outputs.from_version }} 64 | to_version: ${{ steps.step1.outputs.to_version }} 65 | is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }} 66 | is_pre_release: ${{steps.step1.outputs.is_pre_release }} 67 | steps: 68 | - uses: garronej/ts-ci@v2.0.2 69 | id: step1 70 | with: 71 | action_name: is_package_json_version_upgraded 72 | 73 | create_github_release: 74 | runs-on: ubuntu-latest 75 | # We create a release only if the version have been upgraded and we are on the main branch 76 | # or if we are on a branch of the repo that has an PR open on main. 77 | if: | 78 | needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true' && 79 | ( 80 | github.event_name == 'push' || 81 | needs.check_if_version_upgraded.outputs.is_pre_release == 'true' 82 | ) 83 | needs: 84 | - check_if_version_upgraded 85 | steps: 86 | - uses: softprops/action-gh-release@v1 87 | with: 88 | name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }} 89 | tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }} 90 | target_commitish: ${{ github.head_ref || github.ref }} 91 | generate_release_notes: true 92 | draft: false 93 | prerelease: ${{ needs.check_if_version_upgraded.outputs.is_pre_release == 'true' }} 94 | env: 95 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 96 | 97 | publish_on_npm: 98 | runs-on: ubuntu-latest 99 | needs: 100 | - create_github_release 101 | - check_if_version_upgraded 102 | steps: 103 | - uses: actions/checkout@v3 104 | with: 105 | ref: ${{ github.ref }} 106 | - uses: actions/setup-node@v3 107 | with: 108 | registry-url: https://registry.npmjs.org/ 109 | - uses: bahmutov/npm-install@v1 110 | - run: | 111 | PACKAGE_MANAGER=npm 112 | if [ -f "./yarn.lock" ]; then 113 | PACKAGE_MANAGER=yarn 114 | fi 115 | $PACKAGE_MANAGER run build 116 | - run: npx -y -p denoify@1.5.6 enable_short_npm_import_path 117 | env: 118 | DRY_RUN: "0" 119 | - name: Publishing on NPM 120 | run: | 121 | if [ "$(npm show . version)" = "$VERSION" ]; then 122 | echo "This version is already published" 123 | exit 0 124 | fi 125 | if [ "$NODE_AUTH_TOKEN" = "" ]; then 126 | echo "Can't publish on NPM, You must first create a secret called NPM_TOKEN that contains your NPM auth token. https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets" 127 | false 128 | fi 129 | EXTRA_ARGS="" 130 | if [ "$IS_PRE_RELEASE" = "true" ]; then 131 | EXTRA_ARGS="--tag next" 132 | fi 133 | npm publish $EXTRA_ARGS 134 | env: 135 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 136 | VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }} 137 | IS_PRE_RELEASE: ${{ needs.check_if_version_upgraded.outputs.is_pre_release }} 138 | 139 | github_pages: 140 | needs: test 141 | runs-on: ubuntu-latest 142 | if: github.event_name == 'push' 143 | steps: 144 | - uses: actions/checkout@v2 145 | - uses: actions/setup-node@v2.1.3 146 | with: 147 | node-version: '15' 148 | - uses: bahmutov/npm-install@v1 149 | - run: | 150 | yarn build 151 | yarn yarn_link 152 | cd src/test/apps/spa 153 | yarn build 154 | - run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git 155 | env: 156 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 157 | - run: npx -y -p gh-pages@3.1.0 gh-pages -d src/test/apps/spa/build --dest test --add -u "github-actions-bot " -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .vscode 40 | 41 | .DS_Store 42 | 43 | /dist 44 | /.yarn_home 45 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /dist/ 3 | /CHANGELOG.md 4 | /.yarn_home/ 5 | /src/test/apps/ 6 | /src/tools/types/ 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "quoteProps": "preserve", 8 | "trailingComma": "none", 9 | "bracketSpacing": true, 10 | "arrowParens": "avoid" 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 GitHub user u/garronej 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | ✨ Dynamic CSS-in-TS solution, based on Emotion ✨ 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

18 |

19 | Home 20 | - 21 | Documentation 22 | - 23 | Playground 24 |

25 | 26 | You can think of `tss-react` as `@emotion/jss`. 27 | It's, in essence, a type-safe equivalent of [the JSS API](https://cssinjs.org/?v=v10.10.0#react-jss-example) but powered by Emotion, 28 | just like `@emotion/styled` is the [styled-components API](https://styled-components.com/) but powered by Emotion. 29 | 30 | - 🚀 Seamless integration with [MUI](https://mui.com). 31 | - 🌐 Works in [Next.js App and Page Router](https://docs.tss-react.dev/ssr/next.js). 32 | - 🙅‍♂️ No custom styling syntax to learn, no shorthand, just plain CSS. 33 | - 💫 Dynamic Style Generation: TSS enables to generate styles based on the props and internal states of components. 34 | This unfortunately prevents us from supporting [Server Component (RSC)](https://nextjs.org/docs/getting-started/react-essentials#server-components) in Next.js. 35 | We remain hopeful for future support of RSC, contingent on [the provision of a suitable solution by Vercel and React](https://github.com/vercel/next.js/blob/dc6c22c99117bb48beedc4eed402a57b21f03963/docs/02-app/01-building-your-application/04-styling/03-css-in-js.mdx#L10-L12). 36 | If you need RSC support today, you can consider _zero runtime_ solutions like Panda-CSS or Vanilla Extract, 37 | but the expression of complex styles is significantly harder in this paradigm. 38 | - 📚 Your JSX remains readable. Unlike other styling solution that tend to clutter the JSX, TSS enables [isolating the styles from the component structure](https://stackblitz.com/edit/vercel-next-js-bmc6dm?file=ui/TssLogo.tsx). 39 | That been said, sometime it's just easier to inline the styles directly within your components, [TSS enables this as well](https://stackblitz.com/edit/vercel-next-js-bmc6dm?file=ui/TssLogo_intertwined.tsx). 40 | - 🛡️ Eliminate CSS priority conflicts! With TSS you can determine the precedence of multiple classes applied to a component and [arbitrarily increase specificity of some rules](https://docs.tss-react.dev/increase-specificity). 41 | - 🧩 Offers a [type-safe equivalent of the JSS `$` syntax](https://docs.tss-react.dev/nested-selectors). 42 | - ⚙️ Freely customize the underlying `@emotion` cache. 43 | - ✨ Improved [`withStyles`](https://v4.mui.com/styles/api/#withstyles-styles-options-higher-order-component) API featured, to help you migrate away from @material-ui v4. 44 | - 🛠️ Build on top of [`@emotion/react`](https://emotion.sh/docs/@emotion/react), it has very little impact on the bundle size alongside mui (~5kB minziped). 45 | - ⬆️ `'tss-react'` can be used as an advantageous replacement for [@material-ui v4 `makeStyles`](https://material-ui.com/styles/basics/#hook-api) and [`'react-jss'`](https://cssinjs.org/react-jss/?v=v10.9.0). 46 | - 🎯 [Maintained for the foreseeable future](https://github.com/mui-org/material-ui/issues/28463#issuecomment-923085976), issues are dealt with within good delays. 47 | - 📦 Library authors: [`tss-react` won’t be yet another entry in your `peerDependencies`](https://docs.tss-react.dev/publish-a-module-that-uses-tss). 48 | 49 | [demo.webm](https://github.com/garronej/tss-react/assets/6702424/feedb0fc-dd80-46b3-b22f-90d5dd2b36e4) 50 | 51 | > While this module is written in TypeScript, using TypeScript in your application is optional 52 | > (but recommended as it comes with outstanding benefits to both you and your codebase). 53 | 54 |

55 |
56 | Get started 🚀 57 |

58 | 59 | The more ⭐️ the project gets, the more time I spend improving and maintaining it. Thank you for your support 😊 60 | 61 | Needless to mention, this library is heavily inspired by [JSS](https://cssinjs.org/react-jss), the OG CSS-in-JS solution. 62 | 63 | # Development 64 | 65 | Running the demo apps: 66 | 67 | ```bash 68 | git clone https://github.com/garronej/tss-react 69 | cd tss-react 70 | yarn 71 | yarn build 72 | npx tsc -w & npx tsc --module es2015 --outDir dist/esm -w 73 | # Open another Terminal 74 | yarn start_spa # For testing in in a Create React App setup 75 | yarn start_ssr # For testing in a Next.js setup 76 | yarn start_appdir # Next.js 13 setup in App directory mode 77 | ``` 78 | 79 | ## Security contact information 80 | 81 | To report a security vulnerability, please use the 82 | [Tidelift security contact](https://tidelift.com/security). 83 | Tidelift will coordinate the fix and disclosure. 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tss-react", 3 | "version": "4.9.18", 4 | "description": "Type safe CSS-in-JS API heavily inspired by react-jss", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/garronej/tss-react.git" 8 | }, 9 | "main": "dist/index.js", 10 | "types": "dist/index.d.ts", 11 | "module": "dist/esm/index.js", 12 | "exports": { 13 | ".": { 14 | "module": "./dist/esm/index.js", 15 | "default": "./dist/index.js" 16 | }, 17 | "./mui": { 18 | "module": "./dist/esm/mui/index.js", 19 | "default": "./dist/mui/index.js" 20 | }, 21 | "./next/pagesDir": { 22 | "module": "./dist/esm/next/pagesDir.js", 23 | "default": "./dist/next/pagesDir.js" 24 | }, 25 | "./next/appDir": { 26 | "module": "./dist/esm/next/appDir.js", 27 | "default": "./dist/next/appDir.js" 28 | }, 29 | "./dsfr": { 30 | "module": "./dist/esm/dsfr.js", 31 | "default": "./dist/dsfr.js" 32 | }, 33 | "./cssAndCx": { 34 | "module": "./dist/esm/cssAndCx.js", 35 | "default": "./dist/cssAndCx.js" 36 | }, 37 | "./next": { 38 | "module": "./dist/esm/next/index.js", 39 | "default": "./dist/next/index.js" 40 | }, 41 | "./nextJs": { 42 | "module": "./dist/esm/nextJs.js", 43 | "default": "./dist/nextJs.js" 44 | }, 45 | "./compat": { 46 | "module": "./dist/esm/compat.js", 47 | "default": "./dist/compat.js" 48 | }, 49 | "./mui-compat": { 50 | "module": "./dist/esm/mui-compat.js", 51 | "default": "./dist/mui-compat.js" 52 | } 53 | }, 54 | "scripts": { 55 | "build": "tsc && tsc -p tsconfig-esm.json && ts-node src/bin/shim_jsx_element.ts", 56 | "start_spa": "yarn yarn_link && cd src/test/apps/spa && yarn start", 57 | "start_ssr": "yarn yarn_link && cd src/test/apps/ssr && yarn dev", 58 | "start_appdir": "yarn yarn_link && cd src/test/apps/next-appdir && yarn dev", 59 | "lint:check": "eslint . --ext .ts,.tsx", 60 | "lint": "yarn lint:check --fix", 61 | "_format": "prettier '**/*.{ts,tsx,json,md}'", 62 | "format": "yarn _format --write", 63 | "format:check": "yarn _format --list-different", 64 | "yarn_link": "ts-node src/bin/yarn_link.ts" 65 | }, 66 | "lint-staged": { 67 | "*.{ts,tsx}": [ 68 | "eslint --fix" 69 | ], 70 | "*.{ts,tsx,json,md}": [ 71 | "prettier --write" 72 | ] 73 | }, 74 | "husky": { 75 | "hooks": { 76 | "pre-commit": "lint-staged -v" 77 | } 78 | }, 79 | "author": "u/garronej", 80 | "license": "MIT", 81 | "files": [ 82 | "dist/", 83 | "!dist/test/", 84 | "!dist/tsconfig.tsbuildinfo", 85 | "!dist/package.json", 86 | "!dist/esm/test/", 87 | "!dist/esm/tsconfig.tsbuildinfo" 88 | ], 89 | "keywords": [ 90 | "jss", 91 | "hooks", 92 | "react", 93 | "@material-ui", 94 | "mui", 95 | "css", 96 | "makeStyles", 97 | "withStyles" 98 | ], 99 | "homepage": "https://www.tss-react.dev", 100 | "peerDependencies": { 101 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", 102 | "@types/react": "^16.8.0 || ^17.0.2 || ^18.0.0 || ^19.0.0", 103 | "@emotion/react": "^11.4.1", 104 | "@emotion/server": "^11.4.0", 105 | "@mui/material": "^5.0.0 || ^6.0.0 || ^7.0.0" 106 | }, 107 | "peerDependenciesMeta": { 108 | "@emotion/server": { 109 | "optional": true 110 | }, 111 | "@mui/material": { 112 | "optional": true 113 | } 114 | }, 115 | "dependencies": { 116 | "@emotion/serialize": "*", 117 | "@emotion/utils": "*", 118 | "@emotion/cache": "*" 119 | }, 120 | "devDependencies": { 121 | "@codegouvfr/react-dsfr": "^1.2.2", 122 | "@emotion/server": "11.10.0", 123 | "@emotion/react": "11.10.5", 124 | "@mui/material": "5.11.1", 125 | "@emotion/styled": "11.10.5", 126 | "@types/node": "^15.3.1", 127 | "@types/react": "18.0.26", 128 | "@types/react-dom": "18.0.9", 129 | "@typescript-eslint/eslint-plugin": "^4.24.0", 130 | "@typescript-eslint/parser": "^4.24.0", 131 | "eslint": "^7.26.0", 132 | "eslint-config-prettier": "^8.3.0", 133 | "husky": "^4.3.8", 134 | "lint-staged": "^11.0.0", 135 | "next": "13.0.7", 136 | "prettier": "^2.3.0", 137 | "react": "18.1.0", 138 | "ts-node": "^10.2.1", 139 | "tsafe": "^1.6.6", 140 | "typescript": "4.4.4" 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/GlobalStyles.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import * as reactEmotion from "@emotion/react"; 5 | import type { CSSInterpolation } from "./types"; 6 | 7 | export function GlobalStyles(props: { styles: CSSInterpolation }) { 8 | const { styles } = props; 9 | 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /src/bin/shim_jsx_element.ts: -------------------------------------------------------------------------------- 1 | import { transformCodebase } from "./tools/transformCodebase"; 2 | import { join as pathJoin } from "path"; 3 | 4 | function shimJsxElement(params: { distDirPath: string }) { 5 | const { distDirPath } = params; 6 | 7 | transformCodebase({ 8 | srcDirPath: distDirPath, 9 | destDirPath: distDirPath, 10 | transformSourceCode: ({ fileRelativePath, sourceCode }) => { 11 | if (!fileRelativePath.endsWith(".d.ts")) { 12 | return { modifiedSourceCode: sourceCode }; 13 | } 14 | 15 | let modifiedSourceCode = sourceCode.toString("utf8"); 16 | 17 | modifiedSourceCode = modifiedSourceCode.replace( 18 | /JSX\.Element/g, 19 | 'import("react").ReactElement' 20 | ); 21 | 22 | return { 23 | modifiedSourceCode: Buffer.from(modifiedSourceCode, "utf8") 24 | }; 25 | } 26 | }); 27 | } 28 | 29 | shimJsxElement({ 30 | distDirPath: pathJoin(process.cwd(), "dist") 31 | }); 32 | -------------------------------------------------------------------------------- /src/bin/tools/SemVer.ts: -------------------------------------------------------------------------------- 1 | export type SemVer = { 2 | major: number; 3 | minor: number; 4 | patch: number; 5 | rc?: number; 6 | parsedFrom: string; 7 | }; 8 | 9 | export namespace SemVer { 10 | const bumpTypes = ["major", "minor", "patch", "rc", "no bump"] as const; 11 | 12 | export type BumpType = typeof bumpTypes[number]; 13 | 14 | export function parse(versionStr: string): SemVer { 15 | const match = versionStr.match( 16 | /^v?([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:-rc.([0-9]+))?$/ 17 | ); 18 | 19 | if (!match) { 20 | throw new Error(`${versionStr} is not a valid semantic version`); 21 | } 22 | 23 | const semVer: Omit = { 24 | major: parseInt(match[1]), 25 | minor: parseInt(match[2]), 26 | patch: (() => { 27 | const str = match[3]; 28 | 29 | return str === undefined ? 0 : parseInt(str); 30 | })(), 31 | ...(() => { 32 | const str = match[4]; 33 | return str === undefined ? {} : { rc: parseInt(str) }; 34 | })() 35 | }; 36 | 37 | const initialStr = stringify(semVer); 38 | 39 | Object.defineProperty(semVer, "parsedFrom", { 40 | enumerable: true, 41 | get: function () { 42 | const currentStr = stringify(this); 43 | 44 | if (currentStr !== initialStr) { 45 | throw new Error( 46 | `SemVer.parsedFrom can't be read anymore, the version have been modified from ${initialStr} to ${currentStr}` 47 | ); 48 | } 49 | 50 | return versionStr; 51 | } 52 | }); 53 | 54 | return semVer as any; 55 | } 56 | 57 | export function stringify(v: Omit): string { 58 | return `${v.major}.${v.minor}.${v.patch}${ 59 | v.rc === undefined ? "" : `-rc.${v.rc}` 60 | }`; 61 | } 62 | 63 | /** 64 | * 65 | * v1 < v2 => -1 66 | * v1 === v2 => 0 67 | * v1 > v2 => 1 68 | * 69 | */ 70 | export function compare(v1: SemVer, v2: SemVer): -1 | 0 | 1 { 71 | const sign = (diff: number): -1 | 0 | 1 => 72 | diff === 0 ? 0 : diff < 0 ? -1 : 1; 73 | const noUndefined = (n: number | undefined) => n ?? Infinity; 74 | 75 | for (const level of ["major", "minor", "patch", "rc"] as const) { 76 | if (noUndefined(v1[level]) !== noUndefined(v2[level])) { 77 | return sign(noUndefined(v1[level]) - noUndefined(v2[level])); 78 | } 79 | } 80 | 81 | return 0; 82 | } 83 | 84 | /* 85 | console.log(compare(parse("3.0.0-rc.3"), parse("3.0.0")) === -1 ) 86 | console.log(compare(parse("3.0.0-rc.3"), parse("3.0.0-rc.4")) === -1 ) 87 | console.log(compare(parse("3.0.0-rc.3"), parse("4.0.0")) === -1 ) 88 | */ 89 | 90 | export function bumpType(params: { 91 | versionBehind: string | SemVer; 92 | versionAhead: string | SemVer; 93 | }): BumpType | "no bump" { 94 | const versionAhead = 95 | typeof params.versionAhead === "string" 96 | ? parse(params.versionAhead) 97 | : params.versionAhead; 98 | const versionBehind = 99 | typeof params.versionBehind === "string" 100 | ? parse(params.versionBehind) 101 | : params.versionBehind; 102 | 103 | if (compare(versionBehind, versionAhead) === 1) { 104 | throw new Error( 105 | `Version regression ${stringify(versionBehind)} -> ${stringify( 106 | versionAhead 107 | )}` 108 | ); 109 | } 110 | 111 | for (const level of ["major", "minor", "patch", "rc"] as const) { 112 | if (versionBehind[level] !== versionAhead[level]) { 113 | return level; 114 | } 115 | } 116 | 117 | return "no bump"; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/bin/tools/crawl.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { join as pathJoin, relative as pathRelative } from "path"; 3 | 4 | const crawlRec = (dirPath: string, filePaths: string[]) => { 5 | for (const basename of fs.readdirSync(dirPath)) { 6 | const fileOrDirPath = pathJoin(dirPath, basename); 7 | 8 | if (fs.lstatSync(fileOrDirPath).isDirectory()) { 9 | crawlRec(fileOrDirPath, filePaths); 10 | 11 | continue; 12 | } 13 | 14 | filePaths.push(fileOrDirPath); 15 | } 16 | }; 17 | 18 | /** List all files in a given directory return paths relative to the dir_path */ 19 | export function crawl(params: { 20 | dirPath: string; 21 | returnedPathsType: "absolute" | "relative to dirPath"; 22 | }): string[] { 23 | const { dirPath, returnedPathsType } = params; 24 | 25 | const filePaths: string[] = []; 26 | 27 | crawlRec(dirPath, filePaths); 28 | 29 | switch (returnedPathsType) { 30 | case "absolute": 31 | return filePaths; 32 | case "relative to dirPath": 33 | return filePaths.map(filePath => pathRelative(dirPath, filePath)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/bin/tools/fs.rmSync.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { join as pathJoin } from "path"; 3 | import { SemVer } from "./SemVer"; 4 | 5 | /** 6 | * Polyfill of fs.rmSync(dirPath, { "recursive": true }) 7 | * For older version of Node 8 | */ 9 | export function rmSync( 10 | dirPath: string, 11 | options: { recursive: true; force?: true } 12 | ) { 13 | if ( 14 | SemVer.compare(SemVer.parse(process.version), SemVer.parse("14.14.0")) > 15 | 0 16 | ) { 17 | fs.rmSync(dirPath, options); 18 | return; 19 | } 20 | 21 | const { force = true } = options; 22 | 23 | if (force && !fs.existsSync(dirPath)) { 24 | return; 25 | } 26 | 27 | const removeDir_rec = (dirPath: string) => 28 | fs.readdirSync(dirPath).forEach(basename => { 29 | const fileOrDirPath = pathJoin(dirPath, basename); 30 | 31 | if (fs.lstatSync(fileOrDirPath).isDirectory()) { 32 | removeDir_rec(fileOrDirPath); 33 | return; 34 | } else { 35 | fs.unlinkSync(fileOrDirPath); 36 | } 37 | }); 38 | 39 | removeDir_rec(dirPath); 40 | } 41 | -------------------------------------------------------------------------------- /src/bin/tools/transformCodebase.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { crawl } from "./crawl"; 4 | import { rmSync } from "./fs.rmSync"; 5 | 6 | type TransformSourceCode = (params: { 7 | sourceCode: Buffer; 8 | filePath: string; 9 | fileRelativePath: string; 10 | }) => 11 | | { 12 | modifiedSourceCode: Buffer; 13 | newFileName?: string; 14 | } 15 | | undefined; 16 | 17 | /** 18 | * Apply a transformation function to every file of directory 19 | * If source and destination are the same this function can be used to apply the transformation in place 20 | * like filtering out some files or modifying them. 21 | * */ 22 | export function transformCodebase(params: { 23 | srcDirPath: string; 24 | destDirPath: string; 25 | transformSourceCode?: TransformSourceCode; 26 | }) { 27 | const { srcDirPath, transformSourceCode } = params; 28 | 29 | const isTargetSameAsSource = 30 | path.relative(srcDirPath, params.destDirPath) === ""; 31 | 32 | const destDirPath = isTargetSameAsSource 33 | ? path.join(srcDirPath, "..", "tmp_xOsPdkPsTdzPs34sOkHs") 34 | : params.destDirPath; 35 | 36 | fs.mkdirSync(destDirPath, { 37 | recursive: true 38 | }); 39 | 40 | for (const fileRelativePath of crawl({ 41 | dirPath: srcDirPath, 42 | returnedPathsType: "relative to dirPath" 43 | })) { 44 | const filePath = path.join(srcDirPath, fileRelativePath); 45 | const destFilePath = path.join(destDirPath, fileRelativePath); 46 | 47 | // NOTE: Optimization, if we don't need to transform the file, just copy 48 | // it using the lower level implementation. 49 | if (transformSourceCode === undefined) { 50 | fs.mkdirSync(path.dirname(destFilePath), { 51 | recursive: true 52 | }); 53 | 54 | fs.copyFileSync(filePath, destFilePath); 55 | 56 | continue; 57 | } 58 | 59 | const transformSourceCodeResult = transformSourceCode({ 60 | sourceCode: fs.readFileSync(filePath), 61 | filePath, 62 | fileRelativePath 63 | }); 64 | 65 | if (transformSourceCodeResult === undefined) { 66 | continue; 67 | } 68 | 69 | fs.mkdirSync(path.dirname(destFilePath), { 70 | recursive: true 71 | }); 72 | 73 | const { newFileName, modifiedSourceCode } = transformSourceCodeResult; 74 | 75 | fs.writeFileSync( 76 | path.join( 77 | path.dirname(destFilePath), 78 | newFileName ?? path.basename(destFilePath) 79 | ), 80 | modifiedSourceCode 81 | ); 82 | } 83 | 84 | if (isTargetSameAsSource) { 85 | rmSync(srcDirPath, { recursive: true }); 86 | 87 | fs.renameSync(destDirPath, srcDirPath); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/bin/yarn_link.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | 3 | import { execSync } from "child_process"; 4 | import { join as pathJoin, relative as pathRelative } from "path"; 5 | import * as fs from "fs"; 6 | 7 | const tssReactDirPath = pathJoin(__dirname, "..", ".."); 8 | 9 | fs.writeFileSync( 10 | pathJoin(tssReactDirPath, "dist", "package.json"), 11 | Buffer.from( 12 | JSON.stringify( 13 | (() => { 14 | const packageJsonParsed = JSON.parse( 15 | fs 16 | .readFileSync(pathJoin(tssReactDirPath, "package.json")) 17 | .toString("utf8") 18 | ); 19 | 20 | return { 21 | ...packageJsonParsed, 22 | "main": packageJsonParsed["main"].replace(/^dist\//, ""), 23 | "types": packageJsonParsed["types"].replace(/^dist\//, ""), 24 | "module": packageJsonParsed["module"].replace( 25 | /^dist\//, 26 | "" 27 | ), 28 | "exports": Object.fromEntries( 29 | Object.entries(packageJsonParsed["exports"]).map( 30 | ([path, obj]) => [ 31 | path, 32 | Object.fromEntries( 33 | Object.entries( 34 | obj as Record 35 | ).map(([type, path]) => [ 36 | type, 37 | path.replace(/^\.\/dist\//, "./") 38 | ]) 39 | ) 40 | ] 41 | ) 42 | ) 43 | }; 44 | })(), 45 | null, 46 | 2 47 | ), 48 | "utf8" 49 | ) 50 | ); 51 | 52 | const commonThirdPartyDeps = (() => { 53 | const namespaceModuleNames = ["@emotion"]; 54 | const standaloneModuleNames = ["react", "@types/react"]; 55 | 56 | return [ 57 | ...namespaceModuleNames 58 | .map(namespaceModuleName => 59 | fs 60 | .readdirSync( 61 | pathJoin( 62 | tssReactDirPath, 63 | "node_modules", 64 | namespaceModuleName 65 | ) 66 | ) 67 | .map( 68 | submoduleName => 69 | `${namespaceModuleName}/${submoduleName}` 70 | ) 71 | ) 72 | .reduce((prev, curr) => [...prev, ...curr], []), 73 | ...standaloneModuleNames 74 | ]; 75 | })(); 76 | 77 | const yarnHomeDirPath = pathJoin(tssReactDirPath, ".yarn_home"); 78 | 79 | fs.rmSync(yarnHomeDirPath, { "recursive": true, "force": true }); 80 | 81 | fs.mkdirSync(yarnHomeDirPath); 82 | 83 | const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => { 84 | const { targetModuleName, cwd } = params; 85 | 86 | const cmd = [ 87 | "yarn", 88 | "link", 89 | ...(targetModuleName !== undefined ? [targetModuleName] : []) 90 | ].join(" "); 91 | 92 | console.log(`$ cd ${pathRelative(tssReactDirPath, cwd) || "."} && ${cmd}`); 93 | 94 | execSync(cmd, { 95 | cwd, 96 | "env": { 97 | ...process.env, 98 | "HOME": yarnHomeDirPath 99 | } 100 | }); 101 | }; 102 | 103 | const testAppNames = ["spa", "ssr", "next-appdir"] as const; 104 | 105 | const getTestAppPath = (testAppName: typeof testAppNames[number]) => 106 | pathJoin(tssReactDirPath, "src", "test", "apps", testAppName); 107 | 108 | testAppNames.forEach(testAppName => 109 | execSync("yarn install", { "cwd": getTestAppPath(testAppName) }) 110 | ); 111 | 112 | console.log("=== Linking common dependencies ==="); 113 | 114 | const total = commonThirdPartyDeps.length; 115 | let current = 0; 116 | 117 | commonThirdPartyDeps.forEach(commonThirdPartyDep => { 118 | current++; 119 | 120 | console.log(`${current}/${total} ${commonThirdPartyDep}`); 121 | 122 | const localInstallPath = pathJoin( 123 | ...[ 124 | tssReactDirPath, 125 | "node_modules", 126 | ...(commonThirdPartyDep.startsWith("@") 127 | ? commonThirdPartyDep.split("/") 128 | : [commonThirdPartyDep]) 129 | ] 130 | ); 131 | 132 | execYarnLink({ "cwd": localInstallPath }); 133 | 134 | testAppNames.forEach(testAppName => 135 | execYarnLink({ 136 | "cwd": getTestAppPath(testAppName), 137 | "targetModuleName": commonThirdPartyDep 138 | }) 139 | ); 140 | }); 141 | 142 | console.log("=== Linking in house dependencies ==="); 143 | 144 | execYarnLink({ "cwd": pathJoin(tssReactDirPath, "dist") }); 145 | 146 | testAppNames.forEach(testAppName => 147 | execYarnLink({ 148 | "cwd": getTestAppPath(testAppName), 149 | "targetModuleName": "tss-react" 150 | }) 151 | ); 152 | -------------------------------------------------------------------------------- /src/compat.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { EmotionCache } from "@emotion/cache"; 4 | export type { CSSInterpolation, CSSObject, Css, Cx, CxArg } from "./types"; 5 | import { createMakeStyles, TssCacheProvider } from "./makeStyles"; 6 | export { createMakeStyles, TssCacheProvider }; 7 | import { createWithStyles } from "./withStyles_compat"; 8 | export { createWithStyles }; 9 | import { createTss } from "./tss"; 10 | export { createTss }; 11 | export type { Tss } from "./tss"; 12 | 13 | /** @see */ 14 | export { keyframes } from "@emotion/react"; 15 | 16 | /** @see */ 17 | export { GlobalStyles } from "./GlobalStyles"; 18 | 19 | /** @see */ 20 | export function createMakeAndWithStyles(params: { 21 | useTheme: () => Theme; 22 | cache?: EmotionCache; 23 | }) { 24 | return { 25 | ...createMakeStyles(params), 26 | ...createWithStyles(params) 27 | }; 28 | } 29 | 30 | export const { tss } = createTss({ 31 | "useContext": () => ({}) 32 | }); 33 | 34 | export const useStyles = tss.create({}); 35 | -------------------------------------------------------------------------------- /src/cssAndCx.ts: -------------------------------------------------------------------------------- 1 | import { classnames } from "./tools/classnames"; 2 | import type { Cx, Css, CSSObject } from "./types"; 3 | import { serializeStyles } from "@emotion/serialize"; 4 | import type { RegisteredCache } from "@emotion/serialize"; 5 | import { insertStyles, getRegisteredStyles } from "@emotion/utils"; 6 | import { useGuaranteedMemo } from "./tools/useGuaranteedMemo"; 7 | import type { EmotionCache } from "@emotion/cache"; 8 | import { matchCSSObject } from "./types"; 9 | 10 | export const { createCssAndCx } = (() => { 11 | function merge(registered: RegisteredCache, css: Css, className: string) { 12 | const registeredStyles: string[] = []; 13 | 14 | const rawClassName = getRegisteredStyles( 15 | registered, 16 | registeredStyles, 17 | className 18 | ); 19 | 20 | if (registeredStyles.length < 2) { 21 | return className; 22 | } 23 | 24 | return rawClassName + css(registeredStyles); 25 | } 26 | 27 | function createCssAndCx(params: { cache: EmotionCache }) { 28 | const { cache } = params; 29 | 30 | const css: Css = (...args) => { 31 | const serialized = serializeStyles(args, cache.registered); 32 | insertStyles(cache, serialized, false); 33 | const className = `${cache.key}-${serialized.name}`; 34 | 35 | scope: { 36 | const arg = args[0]; 37 | 38 | if (!matchCSSObject(arg)) { 39 | break scope; 40 | } 41 | 42 | increaseSpecificityToTakePrecedenceOverMediaQueries.saveClassNameCSSObjectMapping( 43 | cache, 44 | className, 45 | arg 46 | ); 47 | } 48 | 49 | return className; 50 | }; 51 | 52 | const cx: Cx = (...args) => { 53 | const className = classnames(args); 54 | 55 | const feat27FixedClassnames = 56 | increaseSpecificityToTakePrecedenceOverMediaQueries.fixClassName( 57 | cache, 58 | className, 59 | css 60 | ); 61 | 62 | return merge(cache.registered, css, feat27FixedClassnames); 63 | //return merge(cache.registered, css, className); 64 | }; 65 | 66 | return { css, cx }; 67 | } 68 | 69 | return { createCssAndCx }; 70 | })(); 71 | 72 | export function createUseCssAndCx(params: { useCache: () => EmotionCache }) { 73 | const { useCache } = params; 74 | 75 | function useCssAndCx() { 76 | const cache = useCache(); 77 | 78 | const { css, cx } = useGuaranteedMemo( 79 | () => createCssAndCx({ cache }), 80 | [cache] 81 | ); 82 | 83 | return { css, cx }; 84 | } 85 | 86 | return { useCssAndCx }; 87 | } 88 | 89 | // https://github.com/garronej/tss-react/issues/27 90 | const increaseSpecificityToTakePrecedenceOverMediaQueries = (() => { 91 | const cssObjectMapByCache = new WeakMap< 92 | EmotionCache, 93 | Map 94 | >(); 95 | 96 | return { 97 | "saveClassNameCSSObjectMapping": ( 98 | cache: EmotionCache, 99 | className: string, 100 | cssObject: CSSObject 101 | ) => { 102 | let cssObjectMap = cssObjectMapByCache.get(cache); 103 | 104 | if (cssObjectMap === undefined) { 105 | cssObjectMap = new Map(); 106 | cssObjectMapByCache.set(cache, cssObjectMap); 107 | } 108 | 109 | cssObjectMap.set(className, cssObject); 110 | }, 111 | "fixClassName": (() => { 112 | function fix( 113 | classNameCSSObjects: [ 114 | string /*className*/, 115 | CSSObject | undefined 116 | ][] 117 | ): (string | CSSObject)[] { 118 | let isThereAnyMediaQueriesInPreviousClasses = false; 119 | 120 | return classNameCSSObjects.map(([className, cssObject]) => { 121 | if (cssObject === undefined) { 122 | return className; 123 | } 124 | 125 | let out: string | CSSObject; 126 | 127 | if (!isThereAnyMediaQueriesInPreviousClasses) { 128 | out = className; 129 | 130 | for (const key in cssObject) { 131 | if (key.startsWith("@media")) { 132 | isThereAnyMediaQueriesInPreviousClasses = true; 133 | break; 134 | } 135 | } 136 | } else { 137 | out = { 138 | "&&": cssObject 139 | }; 140 | } 141 | 142 | return out; 143 | }); 144 | } 145 | 146 | return ( 147 | cache: EmotionCache, 148 | className: string, 149 | css: Css 150 | ): string => { 151 | const cssObjectMap = cssObjectMapByCache.get(cache); 152 | 153 | return classnames( 154 | fix( 155 | className 156 | .split(" ") 157 | .map(className => [ 158 | className, 159 | cssObjectMap?.get(className) 160 | ]) 161 | ).map(classNameOrCSSObject => 162 | typeof classNameOrCSSObject === "string" 163 | ? classNameOrCSSObject 164 | : css(classNameOrCSSObject) 165 | ) 166 | ); 167 | }; 168 | })() 169 | }; 170 | })(); 171 | -------------------------------------------------------------------------------- /src/dsfr.ts: -------------------------------------------------------------------------------- 1 | import { useIsDark } from "@codegouvfr/react-dsfr/useIsDark"; 2 | import { createTss } from "./tss"; 3 | 4 | /** @see */ 5 | export const { tss } = createTss({ 6 | "useContext": function useDsfrContext() { 7 | const { isDark } = useIsDark(); 8 | return { isDark }; 9 | } 10 | }); 11 | 12 | export const useStyles = tss.create({}); 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { EmotionCache } from "@emotion/cache"; 4 | export type { CSSInterpolation, CSSObject, Css, Cx, CxArg } from "./types"; 5 | import { createMakeStyles, TssCacheProvider } from "./makeStyles"; 6 | export { createMakeStyles, TssCacheProvider }; 7 | import { createWithStyles } from "./withStyles"; 8 | export { createWithStyles }; 9 | import { createTss } from "./tss"; 10 | export { createTss }; 11 | export type { Tss } from "./tss"; 12 | 13 | /** @see */ 14 | export { keyframes } from "@emotion/react"; 15 | 16 | /** @see */ 17 | export { GlobalStyles } from "./GlobalStyles"; 18 | 19 | /** @see */ 20 | export function createMakeAndWithStyles(params: { 21 | useTheme: () => Theme; 22 | cache?: EmotionCache; 23 | }) { 24 | return { 25 | ...createMakeStyles(params), 26 | ...createWithStyles(params) 27 | }; 28 | } 29 | 30 | export const { tss } = createTss({ 31 | "useContext": () => ({}) 32 | }); 33 | 34 | export const useStyles = tss.create({}); 35 | -------------------------------------------------------------------------------- /src/makeStyles.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | /* eslint-disable @typescript-eslint/no-explicit-any */ 4 | import React, { useMemo } from "react"; 5 | import type { ReactNode } from "react"; 6 | import { objectFromEntries } from "./tools/polyfills/Object.fromEntries"; 7 | import { objectKeys } from "./tools/objectKeys"; 8 | import type { CSSObject } from "./types"; 9 | import { createUseCssAndCx } from "./cssAndCx"; 10 | import { getDependencyArrayRef } from "./tools/getDependencyArrayRef"; 11 | import { typeGuard } from "./tools/typeGuard"; 12 | import { assert } from "./tools/assert"; 13 | import { mergeClasses } from "./mergeClasses"; 14 | import type { EmotionCache } from "@emotion/cache"; 15 | import { createContext, useContext } from "react"; 16 | import { useMuiThemeStyleOverridesPlugin } from "./mui/themeStyleOverridesPlugin"; 17 | import type { 18 | MuiThemeStyleOverridesPluginParams, 19 | MuiThemeLike 20 | } from "./mui/themeStyleOverridesPlugin"; 21 | // @ts-expect-error: It's not declared but it's there. 22 | import { __unsafe_useEmotionCache } from "@emotion/react"; 23 | 24 | const useContextualCache: () => EmotionCache | null = __unsafe_useEmotionCache; 25 | 26 | let counter = 0; 27 | 28 | export function createMakeStyles(params: { 29 | useTheme: () => Theme; 30 | cache?: EmotionCache; 31 | }) { 32 | const { useTheme, cache: cacheProvidedAtInception } = params; 33 | 34 | const { useCache } = createUseCache({ cacheProvidedAtInception }); 35 | 36 | const { useCssAndCx } = createUseCssAndCx({ useCache }); 37 | 38 | /** returns useStyle. */ 39 | function makeStyles< 40 | Params = void, 41 | RuleNameSubsetReferencableInNestedSelectors extends string = never 42 | >(params?: { name?: string | Record; uniqId?: string }) { 43 | const { name: nameOrWrappedName, uniqId = `${counter++}` } = 44 | params ?? {}; 45 | 46 | const name = 47 | typeof nameOrWrappedName !== "object" 48 | ? nameOrWrappedName 49 | : Object.keys(nameOrWrappedName)[0]; 50 | 51 | return function ( 52 | cssObjectByRuleNameOrGetCssObjectByRuleName: 53 | | (( 54 | theme: Theme, 55 | params: Params, 56 | classes: Record< 57 | RuleNameSubsetReferencableInNestedSelectors, 58 | string 59 | > 60 | ) => Record< 61 | RuleName | RuleNameSubsetReferencableInNestedSelectors, 62 | CSSObject 63 | >) 64 | | Record 65 | ) { 66 | const getCssObjectByRuleName = 67 | typeof cssObjectByRuleNameOrGetCssObjectByRuleName === 68 | "function" 69 | ? cssObjectByRuleNameOrGetCssObjectByRuleName 70 | : () => cssObjectByRuleNameOrGetCssObjectByRuleName; 71 | 72 | return function useStyles( 73 | params: Params, 74 | muiStyleOverridesParams?: MuiThemeStyleOverridesPluginParams["muiStyleOverridesParams"] 75 | ) { 76 | const theme = useTheme(); 77 | 78 | let { css, cx } = useCssAndCx(); 79 | 80 | const cache = useCache(); 81 | 82 | let classes = useMemo(() => { 83 | const refClassesCache: Record = {}; 84 | 85 | type RefClasses = Record< 86 | RuleNameSubsetReferencableInNestedSelectors, 87 | string 88 | >; 89 | 90 | const refClasses = 91 | typeof Proxy !== "undefined" && 92 | new Proxy({} as any, { 93 | "get": (_target, propertyKey) => { 94 | if (typeof propertyKey === "symbol") { 95 | assert(false); 96 | } 97 | 98 | return (refClassesCache[propertyKey] = `${ 99 | cache.key 100 | }-${uniqId}${ 101 | name !== undefined ? `-${name}` : "" 102 | }-${propertyKey}-ref`); 103 | } 104 | }); 105 | 106 | const cssObjectByRuleName = getCssObjectByRuleName( 107 | theme, 108 | params, 109 | refClasses || ({} as RefClasses) 110 | ); 111 | 112 | const classes = objectFromEntries( 113 | objectKeys(cssObjectByRuleName).map(ruleName => { 114 | const cssObject = cssObjectByRuleName[ruleName]; 115 | 116 | if (!cssObject.label) { 117 | cssObject.label = `${ 118 | name !== undefined ? `${name}-` : "" 119 | }${ruleName}`; 120 | } 121 | 122 | return [ 123 | ruleName, 124 | `${css(cssObject)}${ 125 | typeGuard( 126 | ruleName, 127 | ruleName in refClassesCache 128 | ) 129 | ? ` ${refClassesCache[ruleName]}` 130 | : "" 131 | }` 132 | ]; 133 | }) 134 | ) as Record; 135 | 136 | objectKeys(refClassesCache).forEach(ruleName => { 137 | if (ruleName in classes) { 138 | return; 139 | } 140 | 141 | classes[ruleName as RuleName] = 142 | refClassesCache[ruleName]; 143 | }); 144 | 145 | return classes; 146 | }, [cache, css, cx, theme, getDependencyArrayRef(params)]); 147 | 148 | { 149 | const propsClasses = muiStyleOverridesParams?.props 150 | .classes as Record | undefined; 151 | 152 | classes = useMemo( 153 | () => mergeClasses(classes, propsClasses, cx), 154 | [classes, getDependencyArrayRef(propsClasses), cx] 155 | ); 156 | } 157 | 158 | { 159 | const pluginResultWrap = useMuiThemeStyleOverridesPlugin({ 160 | classes, 161 | css, 162 | cx, 163 | "name": name ?? "makeStyle no name", 164 | "idOfUseStyles": uniqId, 165 | muiStyleOverridesParams, 166 | // NOTE: If it's not a Mui Theme the plugin is resilient, it will not crash 167 | "theme": theme as MuiThemeLike 168 | }); 169 | 170 | if (pluginResultWrap.classes !== undefined) { 171 | classes = pluginResultWrap.classes; 172 | } 173 | 174 | if (pluginResultWrap.css !== undefined) { 175 | css = pluginResultWrap.css; 176 | } 177 | 178 | if (pluginResultWrap.cx !== undefined) { 179 | cx = pluginResultWrap.cx; 180 | } 181 | } 182 | 183 | return { 184 | classes, 185 | theme, 186 | css, 187 | cx 188 | }; 189 | }; 190 | }; 191 | } 192 | 193 | function useStyles() { 194 | const theme = useTheme(); 195 | const { css, cx } = useCssAndCx(); 196 | return { theme, css, cx }; 197 | } 198 | 199 | return { makeStyles, useStyles }; 200 | } 201 | 202 | const reactContext = createContext(undefined); 203 | 204 | export function TssCacheProvider(props: { 205 | value: EmotionCache; 206 | children: ReactNode; 207 | }) { 208 | const { children, value } = props; 209 | 210 | return ( 211 | {children} 212 | ); 213 | } 214 | 215 | export const { createUseCache } = (() => { 216 | function useCacheProvidedByProvider() { 217 | const cacheExplicitlyProvidedForTss = useContext(reactContext); 218 | 219 | return cacheExplicitlyProvidedForTss; 220 | } 221 | 222 | function createUseCache(params: { 223 | cacheProvidedAtInception?: EmotionCache; 224 | }) { 225 | const { cacheProvidedAtInception } = params; 226 | 227 | function useCache() { 228 | const contextualCache = useContextualCache(); 229 | 230 | const cacheExplicitlyProvidedForTss = useCacheProvidedByProvider(); 231 | 232 | const cacheToBeUsed = 233 | cacheProvidedAtInception ?? 234 | cacheExplicitlyProvidedForTss ?? 235 | contextualCache; 236 | 237 | if (cacheToBeUsed === null) { 238 | throw new Error( 239 | [ 240 | "In order to get SSR working with tss-react you need to explicitly provide an Emotion cache.", 241 | "MUI users be aware: This is not an error strictly related to tss-react, with or without tss-react,", 242 | "MUI needs an Emotion cache to be provided for SSR to work.", 243 | "Here is the MUI documentation related to SSR setup: https://mui.com/material-ui/guides/server-rendering/", 244 | "TSS provides helper that makes the process of setting up SSR easier: https://docs.tss-react.dev/ssr" 245 | ].join("\n") 246 | ); 247 | } 248 | 249 | return cacheToBeUsed; 250 | } 251 | 252 | return { useCache }; 253 | } 254 | 255 | return { createUseCache }; 256 | })(); 257 | -------------------------------------------------------------------------------- /src/mergeClasses.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | 4 | import type { Cx } from "./types"; 5 | import { objectKeys } from "./tools/objectKeys"; 6 | 7 | export function mergeClasses( 8 | classesFromUseStyles: Record, 9 | classesOverrides: Partial> | undefined, 10 | cx: Cx 11 | ): Record & 12 | (string extends U ? {} : Partial, string>>) { 13 | //NOTE: We use this test to be resilient in case classesOverrides is not of the expected type. 14 | if (!(classesOverrides instanceof Object)) { 15 | return classesFromUseStyles as any; 16 | } 17 | 18 | const out: Record = {} as any; 19 | 20 | objectKeys(classesFromUseStyles).forEach( 21 | ruleName => 22 | (out[ruleName] = cx( 23 | classesFromUseStyles[ruleName], 24 | classesOverrides[ruleName] 25 | )) 26 | ); 27 | 28 | objectKeys(classesOverrides).forEach(ruleName => { 29 | if (ruleName in classesFromUseStyles) { 30 | return; 31 | } 32 | 33 | const className = classesOverrides[ruleName]; 34 | 35 | //...Same here, that why we don't do className === undefined 36 | if (typeof className !== "string") { 37 | return; 38 | } 39 | 40 | out[ruleName] = className; 41 | }); 42 | 43 | return out; 44 | } 45 | -------------------------------------------------------------------------------- /src/mui-compat.ts: -------------------------------------------------------------------------------- 1 | import { useTheme } from "@mui/material/styles"; 2 | import { createMakeAndWithStyles } from "./compat"; 3 | export { useStyles, tss } from "./mui"; 4 | 5 | /** @see */ 6 | export const { makeStyles, withStyles } = createMakeAndWithStyles({ 7 | useTheme 8 | }); 9 | -------------------------------------------------------------------------------- /src/mui/index.ts: -------------------------------------------------------------------------------- 1 | export { tss, makeStyles, useStyles, withStyles } from "./mui"; 2 | export { useMuiThemeStyleOverridesPlugin } from "./themeStyleOverridesPlugin"; 3 | export type { MuiThemeStyleOverridesPluginParams } from "./themeStyleOverridesPlugin"; 4 | -------------------------------------------------------------------------------- /src/mui/mui.ts: -------------------------------------------------------------------------------- 1 | import { useTheme } from "@mui/material/styles"; 2 | import { createMakeAndWithStyles } from "../index"; 3 | import { createTss } from "../tss"; 4 | import { useMuiThemeStyleOverridesPlugin } from "./themeStyleOverridesPlugin"; 5 | 6 | /** @see */ 7 | export const { makeStyles, withStyles } = createMakeAndWithStyles({ 8 | useTheme 9 | }); 10 | 11 | export const { tss } = createTss({ 12 | "useContext": function useContext() { 13 | const theme = useTheme(); 14 | return { theme }; 15 | }, 16 | "usePlugin": useMuiThemeStyleOverridesPlugin 17 | }); 18 | 19 | export const useStyles = tss.create({}); 20 | -------------------------------------------------------------------------------- /src/mui/themeStyleOverridesPlugin.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import type { CSSInterpolation } from "../types"; 3 | import { getDependencyArrayRef } from "../tools/getDependencyArrayRef"; 4 | import { mergeClasses } from "../mergeClasses"; 5 | import type { Tss } from "../tss"; 6 | 7 | export type MuiThemeStyleOverridesPluginParams = { 8 | muiStyleOverridesParams?: { 9 | props: Record; 10 | ownerState?: Record; 11 | }; 12 | }; 13 | 14 | export type MuiThemeLike = { 15 | components?: Record; 16 | }; 17 | 18 | export const useMuiThemeStyleOverridesPlugin: Tss.UsePlugin< 19 | { theme: MuiThemeLike }, 20 | MuiThemeStyleOverridesPluginParams 21 | > = ({ classes, theme, muiStyleOverridesParams, css, cx, name }) => { 22 | require_named: { 23 | // NOTE: Hack for backwards compatibility with the makeStyles API. 24 | if (name === "makeStyle no name") { 25 | name = undefined; 26 | break require_named; 27 | } 28 | 29 | if (muiStyleOverridesParams !== undefined && name === undefined) { 30 | throw new Error( 31 | "To use muiStyleOverridesParams, you must specify a name using .withName('MyComponent')" 32 | ); 33 | } 34 | } 35 | 36 | let styleOverrides: 37 | | Record< 38 | string /* E.g: "root" */, 39 | | CSSInterpolation 40 | | ((params: { 41 | ownerState: any; 42 | theme: MuiThemeLike; 43 | }) => CSSInterpolation) 44 | > 45 | | undefined = undefined; 46 | 47 | try { 48 | styleOverrides = 49 | name === undefined 50 | ? undefined 51 | : theme.components?.[name as "MuiAccordion" /*example*/] 52 | ?.styleOverrides || undefined; 53 | 54 | // eslint-disable-next-line no-empty 55 | } catch {} 56 | 57 | const classesFromThemeStyleOverrides = useMemo(() => { 58 | if (styleOverrides === undefined) { 59 | return undefined; 60 | } 61 | 62 | const themeClasses: Record = {}; 63 | 64 | for (const ruleName in styleOverrides) { 65 | const cssObjectOrGetCssObject = styleOverrides[ruleName]; 66 | 67 | if (!(cssObjectOrGetCssObject instanceof Object)) { 68 | continue; 69 | } 70 | 71 | themeClasses[ruleName] = css( 72 | typeof cssObjectOrGetCssObject === "function" 73 | ? cssObjectOrGetCssObject({ 74 | theme, 75 | "ownerState": muiStyleOverridesParams?.ownerState, 76 | ...muiStyleOverridesParams?.props 77 | }) 78 | : cssObjectOrGetCssObject 79 | ); 80 | } 81 | 82 | return themeClasses; 83 | }, [ 84 | styleOverrides, 85 | getDependencyArrayRef(muiStyleOverridesParams?.props), 86 | getDependencyArrayRef(muiStyleOverridesParams?.ownerState), 87 | css 88 | ]); 89 | 90 | classes = useMemo( 91 | () => mergeClasses(classes, classesFromThemeStyleOverrides, cx), 92 | [classes, classesFromThemeStyleOverrides, cx] 93 | ); 94 | 95 | return { classes }; 96 | }; 97 | -------------------------------------------------------------------------------- /src/next/appDir.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import createCache from "@emotion/cache"; 5 | import { useServerInsertedHTML } from "next/navigation"; 6 | import { useState } from "react"; 7 | import { CacheProvider as DefaultCacheProvider } from "@emotion/react"; 8 | import type { Options as OptionsOfCreateCache } from "@emotion/cache"; 9 | import type { EmotionCache } from "@emotion/cache"; 10 | import type { ReactNode } from "react"; 11 | 12 | export type NextAppDirEmotionCacheProviderProps = { 13 | /** This is the options passed to createCache() from 'import createCache from "@emotion/cache"' */ 14 | options: Omit & { 15 | prepend?: boolean; 16 | }; 17 | /** By default from 'import { CacheProvider } from "@emotion/react"' */ 18 | CacheProvider?: React.Provider; 19 | children: ReactNode; 20 | }; 21 | 22 | export function NextAppDirEmotionCacheProvider( 23 | props: NextAppDirEmotionCacheProviderProps 24 | ) { 25 | const { 26 | options: optionsWithPrepend, 27 | CacheProvider = DefaultCacheProvider, 28 | children 29 | } = props; 30 | 31 | const { prepend = false, ...options } = optionsWithPrepend; 32 | 33 | const [{ cache, flush }] = useState(() => { 34 | const cache = createCache(options); 35 | cache.compat = true; 36 | const prevInsert = cache.insert; 37 | let inserted: { name: string; isGlobal: boolean }[] = []; 38 | cache.insert = (...args) => { 39 | const [selector, serialized] = args; 40 | if (cache.inserted[serialized.name] === undefined) { 41 | inserted.push({ 42 | "name": serialized.name, 43 | "isGlobal": selector === "" 44 | }); 45 | } 46 | return prevInsert(...args); 47 | }; 48 | const flush = () => { 49 | const prevInserted = inserted; 50 | inserted = []; 51 | return prevInserted; 52 | }; 53 | return { cache, flush }; 54 | }); 55 | 56 | useServerInsertedHTML(() => { 57 | const inserted = flush(); 58 | if (inserted.length === 0) { 59 | return null; 60 | } 61 | let styles = ""; 62 | let dataEmotionAttribute = cache.key; 63 | 64 | const globals: { 65 | name: string; 66 | style: string; 67 | }[] = []; 68 | 69 | for (const { name, isGlobal } of inserted) { 70 | const style = cache.inserted[name]; 71 | 72 | if (typeof style === "boolean") { 73 | continue; 74 | } 75 | 76 | if (isGlobal) { 77 | globals.push({ name, style }); 78 | } else { 79 | styles += style; 80 | dataEmotionAttribute += ` ${name}`; 81 | } 82 | } 83 | 84 | const get__Html = (style: string) => 85 | prepend ? `@layer emotion {${style}}` : style; 86 | 87 | return ( 88 | <> 89 | {globals.map(({ name, style }) => ( 90 | ` 28 | ); 29 | 30 | }, 31 | [isDark] 32 | ); 33 | 34 | const theme = useMemo( 35 | () => createTheme({ 36 | "palette": { 37 | "mode": isDark ? "dark" : "light", 38 | "primary": { 39 | "main": "#32CD32" //Limegreen 40 | }, 41 | "info": { 42 | "main": "#ffff00" //Yellow 43 | } 44 | }, 45 | "typography": { 46 | "subtitle2": { 47 | "fontStyle": "italic" 48 | } 49 | }, 50 | "components": { 51 | "TestingStyleOverrides": { 52 | "styleOverrides": { 53 | "lightBulb": ({ theme, "ownerState": { isOn }, lightBulbBorderColor }: any) => ({ 54 | "border": `1px solid ${lightBulbBorderColor}`, 55 | "backgroundColor": isOn ? theme.palette.info.main : "grey" 56 | }) 57 | } 58 | 59 | } 60 | } as any 61 | }), 62 | [isDark] 63 | ); 64 | 65 | const wrap = useMemo( 66 | () => ({ isDark, setIsDark }), 67 | [isDark] 68 | ); 69 | 70 | return ( 71 | 72 | 73 | {children} 74 | 75 | 76 | ); 77 | 78 | } 79 | 80 | export function useIsDark() { 81 | const wrap = useContext(contextIsDark); 82 | 83 | if (wrap === undefined) { 84 | throw new Error("Not wrapped in provider"); 85 | } 86 | 87 | return wrap; 88 | 89 | } -------------------------------------------------------------------------------- /src/test/apps/next-appdir/shared/tss-mui.ts: -------------------------------------------------------------------------------- 1 | // NOTE: We can't test directly with tss-react/mui because there 2 | // there is a dual package situation that occurs when linking tss-react locally. 3 | // It will work fine in actual projects though. 4 | 5 | import { useTheme } from "@mui/material/styles"; 6 | import { createMakeAndWithStyles, createTss } from "tss-react"; 7 | import { useMuiThemeStyleOverridesPlugin } from "tss-react/mui"; 8 | 9 | /** @see */ 10 | export const { makeStyles, withStyles } = createMakeAndWithStyles({ 11 | useTheme 12 | }); 13 | 14 | export const { tss } = createTss({ 15 | "useContext": function useContext() { 16 | const theme = useTheme(); 17 | return { theme }; 18 | }, 19 | "usePlugin": useMuiThemeStyleOverridesPlugin 20 | }); 21 | 22 | export const useStyles = tss.create({}); -------------------------------------------------------------------------------- /src/test/apps/next-appdir/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "paths": { 22 | "#/*": [ 23 | "./*" 24 | ] 25 | }, 26 | "plugins": [ 27 | { 28 | "name": "next" 29 | } 30 | ] 31 | }, 32 | "include": [ 33 | "next-env.d.ts", 34 | "**/*.ts", 35 | "**/*.tsx", 36 | ".next/types/**/*.ts" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/test/apps/spa/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | /pages/shared 26 | /.yarn_home -------------------------------------------------------------------------------- /src/test/apps/spa/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /src/test/apps/spa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "https://www.tss-react.dev/test", 3 | "name": "spa", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@types/node": "^15.3.1", 8 | "@types/react": "18.0.26", 9 | "@types/react-dom": "18.0.9", 10 | "react": "18.2.0", 11 | "react-dom": "18.2.0", 12 | "react-scripts": "4.0.3", 13 | "typescript": "^4.9.4", 14 | "tss-react": "file:../../../../dist", 15 | "@emotion/react": "11.10.5", 16 | "@emotion/styled": "11.10.5", 17 | "@mui/material": "5.11.1", 18 | "@mui/icons-material": "^5.11.0" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all", 37 | "IE 11" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/apps/spa/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garronej/tss-react/cfba19351eae67cc71ba61cf88f88a8e00fdb5e7/src/test/apps/spa/public/favicon.ico -------------------------------------------------------------------------------- /src/test/apps/spa/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/test/apps/spa/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garronej/tss-react/cfba19351eae67cc71ba61cf88f88a8e00fdb5e7/src/test/apps/spa/public/logo192.png -------------------------------------------------------------------------------- /src/test/apps/spa/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garronej/tss-react/cfba19351eae67cc71ba61cf88f88a8e00fdb5e7/src/test/apps/spa/public/logo512.png -------------------------------------------------------------------------------- /src/test/apps/spa/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/test/apps/spa/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/test/apps/spa/src/App.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { useReducer, memo } from "react"; 3 | import { makeStyles, withStyles } from "makeStyles"; 4 | import { GlobalStyles } from "tss-react"; 5 | import { styled } from "@mui/material"; 6 | import Button from "@mui/material/Button" 7 | import Breadcrumbs from "@mui/material/Breadcrumbs"; 8 | import type { CSSObject } from "tss-react"; 9 | import Typography from "@mui/material/Typography"; 10 | import InputBase from "@mui/material/InputBase"; 11 | import Tab from "@mui/material/Tab"; 12 | import PhoneIcon from "@mui/icons-material/Phone"; 13 | 14 | const H1 = styled('h1')({ 15 | "color": "yellow" 16 | }); 17 | 18 | export function App(props: { className?: string; }) { 19 | const { className } = props; 20 | const { classes, css, cx } = useStyles(); 21 | 22 | return ( 23 | <> 24 | 34 |
35 |

Black

36 |

Should be lime green

37 |

43 | Black, should have border and shadow 44 |

45 |

Should be cyan

46 |

Should be yellow

47 |

Should be dark red

48 | 49 | 56 |
57 |
58 | Background should turn red when mouse is hover the parent. 59 |
60 |
61 | 62 | 66 | background should be lightblue 67 | and the separator (/) should be red except when hover, then it is blue 68 | 69 |
70 | 77 | background should be lightblue 78 | and the separator (/) should be red except when hover, then it is blue 79 | 80 | 81 | 82 | The separator 83 | should be lightgreen 84 | 85 | 86 | 93 | 94 | 101 |
102 | Background should be cyan 103 |
104 |
105 | Background should be yellow 106 |
107 | 108 | 109 | 110 |
111 | 112 |
116 | Background should turn cyan when mouse hover the parent. 117 | Also the text should NOT be pink 118 |
119 |
120 | Background should NOT turn cyan when mouse hover the parent. 121 |
122 | 123 |
124 | 125 |
126 | The background color should turn from lightgreen to cyan when the window 127 | inner with goes is below 960px 128 |
129 | 130 | 134 | 135 | The text should turn from red to blue when the 136 | window inner width goes below 960px 137 | And I should have a class like tss-xxxxxx-MyStyledButton-text 138 | 139 |
140 | 141 | Background should be red 142 | 143 | 144 | Background should be limegreen 145 | 146 |
157 | background should be lightgreen 158 |
159 | 160 | 161 | 162 | 163 | 173 | } label="Background should be greenish" /> 174 | 175 |
176 | 177 | ); 178 | } 179 | 180 | const useStyles = makeStyles({ 181 | "name": { App }, 182 | })((theme, _params, classes) => { 183 | 184 | const childRefTest_wrapper2 = { 185 | "border": "1px solid black", 186 | "margin": 30, 187 | "height": 100, 188 | "color": "black" 189 | }; 190 | 191 | return { 192 | "root": { 193 | "& > h1:nth-child(2)": { 194 | "color": theme.palette.primary.main, 195 | } 196 | }, 197 | "ovStyled": { 198 | "color": "darkred" 199 | }, 200 | "ovInternal": { 201 | "backgroundColor": "darkblue" 202 | }, 203 | "parent": { 204 | "border": "1px solid black", 205 | "padding": 30, 206 | [`&:hover .${classes.child}`]: { 207 | "background": "red", 208 | } 209 | }, 210 | "child": { 211 | "background": "blue", 212 | "border": "1px solid black" 213 | }, 214 | "breadcrumbs_className": { 215 | "backgroundColor": "lightblue", 216 | "& .MuiBreadcrumbs-separator": { 217 | "color": "red" 218 | }, 219 | "&:hover .MuiBreadcrumbs-separator": { 220 | "color": "blue" 221 | } 222 | }, 223 | 224 | "breadcrumbs2_root": { 225 | "backgroundColor": "lightblue", 226 | [`&:hover .${classes.breadcrumbs2_separator}`]: { 227 | "color": "blue" 228 | } 229 | }, 230 | "breadcrumbs2_separator": { 231 | "color": "red" 232 | }, 233 | 234 | "button2_className": { 235 | "backgroundColor": "red" 236 | }, 237 | 238 | "button2_root": { 239 | "backgroundColor": "red" 240 | }, 241 | 242 | "testCx_bgYellow": { 243 | "backgroundColor": "yellow" 244 | }, 245 | "testCx_bgCyan": { 246 | "backgroundColor": "cyan" 247 | }, 248 | 249 | "childRefTest_wrapper": { 250 | "border": "1px solid black", 251 | [`&:hover .${classes.childRefTest_wrapper1}`]: { 252 | "backgroundColor": "cyan" 253 | } 254 | }, 255 | "childRefTest_wrapper1": { 256 | ...childRefTest_wrapper2 257 | }, 258 | childRefTest_wrapper2, 259 | "childRefTest_textColorPink": { 260 | "color": "pink" 261 | }, 262 | "mq": { 263 | "height": 100, 264 | "backgroundColor": "lightgreen", 265 | "@media (max-width: 960px)": { 266 | "backgroundColor": "cyan" 267 | } 268 | } 269 | }; 270 | }); 271 | 272 | 273 | function MyComponent(props: { className?: string; colorSmall: string; }) { 274 | const classes = withStyles.getClasses(props); 275 | return ( 276 |
277 | The background color should turn from limegreen to cyan when the window 278 | inner with goes below 960px. 279 | Font should be red 280 |
281 | ); 282 | } 283 | 284 | const MyComponentStyled = withStyles( 285 | MyComponent, 286 | (theme, props) => ({ 287 | "root": { 288 | "backgroundColor": theme.palette.primary.main, 289 | "height": 100, 290 | "marginTop": 20 291 | }, 292 | "@media (max-width: 960px)": { 293 | "root": { 294 | "backgroundColor": props.colorSmall 295 | }, 296 | } 297 | }) 298 | ); 299 | 300 | const MyStyledButton = withStyles( 301 | Button, 302 | { 303 | "text": { 304 | "color": "red", 305 | "textTransform": "unset" 306 | }, 307 | "@media (max-width: 960px)": { 308 | "text": { 309 | "color": "blue" 310 | }, 311 | } 312 | }, 313 | { "name": "MyStyledButton" } 314 | ); 315 | 316 | const MyAnchorStyled = withStyles( 317 | "a", 318 | (theme, { href }) => ({ 319 | "root": { 320 | "border": "1px solid black", 321 | "backgroundColor": 322 | href?.startsWith("https") ? 323 | theme.palette.primary.main : 324 | "red" 325 | } 326 | }) 327 | ); 328 | 329 | const MyBreadcrumbs = withStyles( 330 | Breadcrumbs, 331 | (theme, _props, classes) => ({ 332 | "ol": { 333 | [`& .${classes.separator}`]: { 334 | "color": theme.palette.primary.main 335 | } 336 | } 337 | }) 338 | ); 339 | 340 | const { SecondNestedSelectorExample } = (() => { 341 | 342 | const SecondNestedSelectorExample = memo(() => { 343 | 344 | const { classes, cx } = useStyles({ "color": "primary" }); 345 | 346 | return ( 347 |
348 |
349 | The Background take the primary theme color when the mouse is hover the parent. 350 |
351 |
352 | The Background take the primary theme color when the mouse is hover the parent. 353 | I am smaller than the other child. 354 |
355 |
356 | ); 357 | 358 | }); 359 | 360 | const useStyles = makeStyles<{ color: "primary" | "secondary" }, "child" | "small">({ 361 | "name": { SecondNestedSelectorExample } 362 | })( 363 | (theme, { color }, classes) => ({ 364 | "root": { 365 | "padding": 30, 366 | [`&:hover .${classes.child}`]: { 367 | "backgroundColor": theme.palette[color].main, 368 | }, 369 | }, 370 | "small": {}, 371 | "child": { 372 | "border": "1px solid black", 373 | "height": 50, 374 | [`&.${classes.small}`]: { 375 | "height": 30, 376 | } 377 | }, 378 | }) 379 | ); 380 | 381 | return { SecondNestedSelectorExample }; 382 | 383 | })() 384 | 385 | const { MyTestComponentForMergedClasses } = (() => { 386 | 387 | const useStyles = makeStyles()({ 388 | "foo": { 389 | "border": "3px dotted black", 390 | "backgroundColor": "red" 391 | }, 392 | "bar": { 393 | "color": "pink" 394 | } 395 | }); 396 | 397 | type Props = { 398 | classes?: Partial["classes"]>; 399 | }; 400 | 401 | const MyTestComponentForMergedClassesInternal = (props: Props) => { 402 | 403 | const { classes } = useStyles(undefined, { props }); 404 | 405 | return ( 406 |
407 | The background should be green, the box should have a dotted border and the text should be pink 408 |
409 | ); 410 | 411 | }; 412 | 413 | 414 | const MyTestComponentForMergedClasses = () => { 415 | 416 | const { css } = useStyles(); 417 | 418 | return ; 419 | 420 | }; 421 | 422 | return { MyTestComponentForMergedClasses }; 423 | 424 | })(); 425 | 426 | const { TestCastingMuiTypographyStyleToCSSObject } = (() => { 427 | 428 | const useStyles = makeStyles()(theme => ({ 429 | "root": { 430 | ...(theme.typography.subtitle2 as CSSObject), 431 | "color": "red" 432 | } 433 | })); 434 | 435 | const TestCastingMuiTypographyStyleToCSSObject = () => { 436 | 437 | const { classes } = useStyles(); 438 | 439 | return ( 440 | <> 441 | This text should be italic 442 | This text should be italic and red 443 | 444 | ); 445 | 446 | }; 447 | 448 | return { TestCastingMuiTypographyStyleToCSSObject }; 449 | 450 | })(); 451 | 452 | const { TestPr54 } = (() => { 453 | 454 | const CustomInputBase = withStyles( 455 | InputBase, 456 | theme => 457 | ({ 458 | "input": { 459 | "backgroundColor": theme.palette.grey[50], 460 | }, 461 | } as const) 462 | ); 463 | 464 | const TestPr54 = withStyles(CustomInputBase, () => ({ 465 | "input": { 466 | "backgroundColor": "red" 467 | }, 468 | })); 469 | 470 | return { TestPr54 }; 471 | 472 | })(); 473 | 474 | const { TestingStyleOverrides } = (() => { 475 | 476 | type Props = { 477 | className?: string; 478 | classes?: Partial["classes"]>; 479 | lightBulbBorderColor: string; 480 | } 481 | 482 | function TestingStyleOverrides(props: Props) { 483 | 484 | const { className } = props; 485 | 486 | const [isOn, toggleIsOn] = useReducer(isOn => !isOn, false); 487 | 488 | const { classes, cx } = useStyles(undefined, { props, "ownerState": { isOn } }); 489 | 490 | return ( 491 |
492 |
493 | 494 |

Div should have a border, background should be white

495 |

Light bulb should have black border, it should be yellow when turned on.

496 |
497 | ); 498 | 499 | } 500 | 501 | const useStyles = makeStyles({ "name": { TestingStyleOverrides } })(theme => ({ 502 | "root": { 503 | "border": "1px solid black", 504 | "width": 500, 505 | "height": 200, 506 | "position": "relative", 507 | "color": "black" 508 | }, 509 | "lightBulb": { 510 | "position": "absolute", 511 | "width": 50, 512 | "height": 50, 513 | "top": 120, 514 | "left": 500 / 2 - 50, 515 | "borderRadius": "50%" 516 | } 517 | })); 518 | 519 | return { TestingStyleOverrides }; 520 | 521 | })(); 522 | 523 | const StyledTab = withStyles(Tab, { 524 | "root": { 525 | "backgroundColor": "red" 526 | }, 527 | "labelIcon": { 528 | "backgroundColor": "green" 529 | } 530 | }); 531 | 532 | const TestWithStylesWithClassComponents = (() => { 533 | 534 | type Props = { 535 | className?: string; 536 | classes?: Partial>; 537 | isBig: boolean; 538 | }; 539 | 540 | class MyComponent extends React.Component { 541 | render() { 542 | 543 | const classes = withStyles.getClasses(this.props) 544 | 545 | return ( 546 |
547 | Background color should be green on big screen, red on small screen, there is a black border on the text 548 |
549 | ); 550 | } 551 | } 552 | 553 | const MyComponentStyled = withStyles( 554 | MyComponent, 555 | (theme, props) => ({ 556 | "root": { 557 | "backgroundColor": theme.palette.primary.main, 558 | "height": props.isBig ? 200 : undefined 559 | }, 560 | "span": { 561 | "border": "1px solid black" 562 | }, 563 | "@media (max-width: 960px)": { 564 | "root": { 565 | "backgroundColor": "red" 566 | } 567 | } 568 | }) 569 | ); 570 | 571 | return MyComponentStyled; 572 | 573 | })(); 574 | -------------------------------------------------------------------------------- /src/test/apps/spa/src/index.tsx: -------------------------------------------------------------------------------- 1 | import "react-app-polyfill/ie11"; 2 | import "react-app-polyfill/stable"; 3 | import { StrictMode } from "react"; 4 | //NOTE: If makeStyles was located in src/app we would write: import { makeStyles } from "app/makeStyles"; 5 | import { useStyles } from "makeStyles"; 6 | import { App } from "./App"; 7 | import { CacheProvider } from "@emotion/react"; 8 | import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles"; 9 | import { createTheme } from "@mui/material/styles"; 10 | import CssBaseline from "@mui/material/CssBaseline"; 11 | import { createRoot } from "react-dom/client"; 12 | import createCache from "@emotion/cache"; 13 | 14 | const muiCache = createCache({ 15 | "key": "mui", 16 | "prepend": true 17 | }); 18 | 19 | const theme = createTheme({ 20 | "palette": { 21 | "primary": { 22 | "main": "#32CD32" //Limegreen 23 | }, 24 | "info": { 25 | "main": "#ffff00" //Yellow 26 | } 27 | }, 28 | "typography": { 29 | "subtitle2": { 30 | "fontStyle": "italic" 31 | } 32 | }, 33 | "components": { 34 | //@ts-ignore 35 | "TestingStyleOverrides": { 36 | "styleOverrides": { 37 | "lightBulb": ({ theme, ownerState: { isOn }, lightBulbBorderColor }: any) => ({ 38 | "border": `1px solid ${lightBulbBorderColor}`, 39 | "backgroundColor": isOn ? theme.palette.info.main : "grey" 40 | }) 41 | } 42 | 43 | } 44 | } 45 | }); 46 | 47 | function Root() { 48 | 49 | const { css } = useStyles(); 50 | 51 | return ; 52 | 53 | } 54 | 55 | createRoot(document.getElementById("root")!).render( 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | -------------------------------------------------------------------------------- /src/test/apps/spa/src/makeStyles.ts: -------------------------------------------------------------------------------- 1 | 2 | import { createMakeAndWithStyles } from "tss-react"; 3 | import { useTheme } from "@mui/material/styles"; 4 | 5 | export const { makeStyles, useStyles, withStyles } = createMakeAndWithStyles({ useTheme }); 6 | -------------------------------------------------------------------------------- /src/test/apps/spa/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/test/apps/spa/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx" 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /src/test/apps/ssr/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /src/test/apps/ssr/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | -------------------------------------------------------------------------------- /src/test/apps/ssr/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # tss-react + MUI setup in a Next.js project 4 | 5 | Run the App: 6 | 7 | ```bash 8 | git clone https://github.com/garronej/tss-react 9 | cd tss-react 10 | yarn 11 | yarn build 12 | yarn start_ssr 13 | ``` 14 | 15 | It's live here: https://next-demo.tss-react.dev -------------------------------------------------------------------------------- /src/test/apps/ssr/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /src/test/apps/ssr/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /src/test/apps/ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssr", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "11.10.5", 13 | "@emotion/server": "11.10.0", 14 | "@emotion/styled": "11.10.5", 15 | "@mui/icons-material": "^5.11.0", 16 | "@mui/material": "5.11.1", 17 | "next": "13.0.7", 18 | "powerhooks": "^0.17.2", 19 | "react": "18.2.0", 20 | "react-dom": "18.2.0", 21 | "tss-react": "file:../../../../dist" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^18.11.17", 25 | "@types/react": "18.0.26", 26 | "eslint": "7.30.0", 27 | "eslint-config-next": "11.0.1", 28 | "typescript": "^4.6.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/apps/ssr/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import Head from "next/head"; 3 | import type { AppProps } from 'next/app' 4 | import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles"; 5 | import { createTheme } from "@mui/material/styles"; 6 | import CssBaseline from "@mui/material/CssBaseline"; 7 | import { useIsDarkModeEnabled, withIsDarkModeEnabled } from "../shared/isDarkModeEnabled"; 8 | import { createEmotionSsrAdvancedApproach } from "tss-react/next/pagesDir"; 9 | 10 | const { 11 | augmentDocumentWithEmotionCache, 12 | withAppEmotionCache 13 | } = createEmotionSsrAdvancedApproach({ "key": "css" }); 14 | 15 | export { augmentDocumentWithEmotionCache }; 16 | 17 | export function App({ Component, pageProps }: AppProps) { 18 | 19 | const { isDarkModeEnabled } = useIsDarkModeEnabled(); 20 | 21 | const theme = useMemo( 22 | () => createTheme({ 23 | "palette": { 24 | "mode": isDarkModeEnabled ? "dark" : "light", 25 | "primary": { 26 | "main": "#32CD32" //Limegreen 27 | }, 28 | "info": { 29 | "main": "#ffff00" //Yellow 30 | } 31 | }, 32 | "typography": { 33 | "subtitle2": { 34 | "fontStyle": "italic" 35 | } 36 | }, 37 | "components": { 38 | "TestingStyleOverrides": { 39 | "styleOverrides": { 40 | "lightBulb": ({ theme, ownerState: { isOn }, lightBulbBorderColor }: any) => ({ 41 | "border": `1px solid ${lightBulbBorderColor}`, 42 | "backgroundColor": isOn ? theme.palette.info.main : "grey" 43 | }) 44 | } 45 | 46 | } 47 | } as any 48 | }), 49 | [isDarkModeEnabled] 50 | ); 51 | 52 | return ( 53 | <> 54 | 55 | Create Next App 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | 66 | } 67 | 68 | export default withAppEmotionCache(withIsDarkModeEnabled(App)); -------------------------------------------------------------------------------- /src/test/apps/ssr/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document from "next/document"; 2 | import { augmentDocumentWithEmotionCache } from "./_app"; 3 | 4 | augmentDocumentWithEmotionCache(Document); 5 | 6 | export default Document; 7 | -------------------------------------------------------------------------------- /src/test/apps/ssr/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garronej/tss-react/cfba19351eae67cc71ba61cf88f88a8e00fdb5e7/src/test/apps/ssr/public/favicon.ico -------------------------------------------------------------------------------- /src/test/apps/ssr/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/test/apps/ssr/shared/isDarkModeEnabled.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import { useEffect } from "react"; 3 | import { createUseSsrGlobalState } from "powerhooks/useSsrGlobalState"; 4 | import { updateSearchBarUrl, retrieveParamFromUrl } from "powerhooks/tools/urlSearchParams"; 5 | import Head from "next/head"; 6 | 7 | export const { useIsDarkModeEnabled, withIsDarkModeEnabled } = createUseSsrGlobalState({ 8 | "name": "isDarkModeEnabled", 9 | "getValueSeverSide": appContext => { 10 | 11 | const { theme } = appContext.router.query; 12 | 13 | if (typeof theme !== "string") { 14 | return undefined; 15 | } 16 | 17 | switch (theme) { 18 | case "dark": return { "value": true }; 19 | case "light": return { "value": false }; 20 | } 21 | 22 | return undefined; 23 | 24 | }, 25 | "getInitialValueServerSide": () => ({ 26 | "doFallbackToGetInitialValueClientSide": true, 27 | "initialValue": false 28 | }), 29 | "getInitialValueClientSide": () => 30 | window.matchMedia && 31 | window.matchMedia("(prefers-color-scheme: dark)").matches, 32 | "Head": ({ isDarkModeEnabled }) => { 33 | 34 | // eslint-disable-next-line react-hooks/rules-of-hooks 35 | useEffect(() => { 36 | 37 | const result = retrieveParamFromUrl({ 38 | "url": window.location.href, 39 | "name": "theme" 40 | }); 41 | 42 | if (!result.wasPresent) { 43 | return; 44 | } 45 | 46 | updateSearchBarUrl(result.newUrl); 47 | 48 | }, []); 49 | 50 | 51 | return ( 52 | 53 | 58 | 59 | ); 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /src/test/apps/ssr/shared/makeStyles.ts: -------------------------------------------------------------------------------- 1 | import { createMakeAndWithStyles } from "tss-react"; 2 | import { useTheme } from "@mui/material/styles"; 3 | 4 | export const { makeStyles, useStyles, withStyles } = createMakeAndWithStyles({ useTheme }); -------------------------------------------------------------------------------- /src/test/apps/ssr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "**/*.ts", 25 | "**/*.tsx" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/test/types/ReactComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import type { ReactComponent } from "../../tools/ReactComponent"; 3 | import { memo } from "react"; 4 | import Button from "@mui/material/Button"; 5 | import type { ButtonProps } from "@mui/material/Button"; 6 | import { assert } from "tsafe/assert"; 7 | import type { Equals } from "tsafe"; 8 | 9 | type ExtractProps> = 10 | Component extends ReactComponent ? Props : never; 11 | 12 | { 13 | type MyComponentProps = { foo: string; bar?: string }; 14 | 15 | function MyComponent(_props: MyComponentProps) { 16 | return
; 17 | } 18 | 19 | assert, MyComponentProps>>(); 20 | } 21 | 22 | { 23 | type MyComponentProps = { foo: string; bar?: string }; 24 | 25 | const MyComponent = memo((_props: MyComponentProps) => { 26 | return
; 27 | }); 28 | 29 | assert, MyComponentProps>>(); 30 | } 31 | 32 | { 33 | type MyComponentProps = { 34 | foo: string; 35 | bar?: string; 36 | }; 37 | 38 | class MyComponent extends Component {} 39 | 40 | assert, MyComponentProps>>(); 41 | } 42 | 43 | { 44 | type Props = ButtonProps; 45 | 46 | const Component = Button; 47 | 48 | assert, Props>>(); 49 | } 50 | -------------------------------------------------------------------------------- /src/test/types/makeStyles.tsx: -------------------------------------------------------------------------------- 1 | import { assert } from "tsafe/assert"; 2 | import type { Equals } from "tsafe"; 3 | import { createMakeStyles } from "../../makeStyles"; 4 | 5 | type Theme = { 6 | _theme_brand: unknown; 7 | }; 8 | 9 | const { makeStyles } = createMakeStyles({ 10 | "useTheme": () => null as unknown as Theme 11 | }); 12 | 13 | const useStyles = makeStyles()((theme, props, classes) => { 14 | assert>>(); 15 | assert>(); 16 | assert>(); 17 | 18 | return { 19 | "root": { 20 | "backgroundColor": "red", 21 | [`& .${classes.xxx}`]: { 22 | "color": "white" 23 | } 24 | }, 25 | "xxx": {} 26 | }; 27 | }); 28 | 29 | const { classes } = useStyles(); 30 | 31 | assert>>(); 32 | 33 | { 34 | const useStyles = makeStyles()((_theme, _props, classes) => { 35 | assert>>(); 36 | 37 | return { 38 | "root": { 39 | "backgroundColor": "red" 40 | } 41 | }; 42 | }); 43 | 44 | const { classes } = useStyles(); 45 | 46 | assert>>(); 47 | } 48 | 49 | makeStyles()( 50 | //@ts-expect-error: "xxx" should be added to the record of CSSObject 51 | (theme, props, refs) => ({ 52 | "root": { 53 | "backgroundColor": "red" 54 | } 55 | }) 56 | ); 57 | 58 | { 59 | const { makeStyles } = createMakeStyles({ 60 | "useTheme": () => null as unknown as Theme 61 | }); 62 | 63 | const useStyles = makeStyles()({ 64 | "root": { 65 | "backgroundColor": "red" 66 | }, 67 | "xxx": {} 68 | }); 69 | 70 | const { classes } = useStyles(); 71 | 72 | assert>>(); 73 | } 74 | -------------------------------------------------------------------------------- /src/test/types/mergeClasses.ts: -------------------------------------------------------------------------------- 1 | import { createMakeAndWithStyles } from "../.."; 2 | import { mergeClasses } from "../../mergeClasses"; 3 | import type { Equals } from "tsafe"; 4 | import { assert } from "tsafe"; 5 | 6 | const { makeStyles } = createMakeAndWithStyles({ 7 | "useTheme": () => ({}) 8 | }); 9 | 10 | const useStyles = makeStyles()({ 11 | "foo": {}, 12 | "bar": {} 13 | }); 14 | 15 | const { classes } = useStyles(); 16 | 17 | const classesOverrides: 18 | | { 19 | foo?: string; 20 | bar?: string; 21 | onlyOnProp?: string; 22 | } 23 | | undefined = null as any; 24 | 25 | { 26 | const mergedClasses = mergeClasses(classes, classesOverrides, null as any); 27 | 28 | assert< 29 | Equals< 30 | typeof mergedClasses, 31 | { 32 | foo: string; 33 | bar: string; 34 | onlyOnProp?: string; 35 | } 36 | > 37 | >(); 38 | } 39 | 40 | { 41 | const mergedClasses = mergeClasses(classes, {}, null as any); 42 | 43 | assert< 44 | Equals< 45 | typeof mergedClasses, 46 | { 47 | foo: string; 48 | bar: string; 49 | } 50 | > 51 | >(); 52 | } 53 | 54 | //@ts-expect-error 55 | mergeClasses(classes, { "foo": 33 }, null as any); 56 | 57 | { 58 | const mergedClasses = mergeClasses( 59 | classes, 60 | { "foo": "xxx", "somethingElse": "zzz" }, 61 | null as any 62 | ); 63 | 64 | assert< 65 | Equals< 66 | typeof mergedClasses, 67 | { 68 | foo: string; 69 | bar: string; 70 | somethingElse?: string; 71 | } 72 | > 73 | >(); 74 | } 75 | { 76 | const mergedClasses = mergeClasses(classes, undefined, null as any); 77 | 78 | assert< 79 | Equals< 80 | typeof mergedClasses, 81 | { 82 | foo: string; 83 | bar: string; 84 | } 85 | > 86 | >(); 87 | } 88 | -------------------------------------------------------------------------------- /src/test/types/tss.tsx: -------------------------------------------------------------------------------- 1 | import { assert } from "tsafe/assert"; 2 | import type { Equals } from "tsafe"; 3 | import { createTss } from "../../tss"; 4 | import { Reflect } from "tsafe/Reflect"; 5 | import type { Cx, Css } from "../../types"; 6 | import { tss as tssMui } from "../../mui"; 7 | 8 | type Context = { 9 | contextProp1: { 10 | _brand_context_prop1: unknown; 11 | }; 12 | contextProp2: { 13 | _brand_context_prop2: unknown; 14 | }; 15 | }; 16 | 17 | const { tss } = createTss({ 18 | "useContext": () => Reflect() 19 | }); 20 | 21 | { 22 | assert< 23 | Equals< 24 | keyof typeof tss, 25 | "create" | "withParams" | "withName" | "withNestedSelectors" 26 | > 27 | >(); 28 | 29 | const tss_1 = tss.withName("MyComponent"); 30 | 31 | assert< 32 | Equals< 33 | keyof typeof tss_1, 34 | "create" | "withParams" | "withNestedSelectors" 35 | > 36 | >(); 37 | 38 | const tss_2 = tss_1.withParams<{ prop1: string }>(); 39 | 40 | assert>(); 41 | 42 | const tss_3 = tss_2.withNestedSelectors<"foo" | "bar">(); 43 | 44 | assert>(); 45 | } 46 | 47 | { 48 | const useStyles = tss 49 | .withNestedSelectors<"xxx">() 50 | .create(({ contextProp1, contextProp2, classes, ...rest }) => { 51 | assert>(); 52 | assert>(); 53 | assert>>(); 54 | assert>(); 55 | 56 | return { 57 | "root": { 58 | "backgroundColor": "red", 59 | [`& .${classes.xxx}`]: { 60 | "color": "white" 61 | } 62 | }, 63 | "xxx": {} 64 | }; 65 | }); 66 | 67 | const { classes, css, cx } = useStyles(); 68 | 69 | assert>>(); 70 | assert>(); 71 | assert>(); 72 | 73 | useStyles({ 74 | "classesOverrides": { 75 | "root": "xxx-yy" 76 | } 77 | }); 78 | } 79 | 80 | { 81 | const useStyles = tss 82 | .withNestedSelectors<"xxx">() 83 | .withParams<{ prop1: { _brand_prop1: unknown } }>() 84 | .create(({ contextProp1, contextProp2, classes, prop1, ...rest }) => { 85 | assert>(); 86 | assert>(); 87 | assert>(); 88 | assert>>(); 89 | assert>(); 90 | 91 | return { 92 | "root": { 93 | "backgroundColor": "red", 94 | [`& .${classes.xxx}`]: { 95 | "color": "white" 96 | } 97 | }, 98 | "xxx": {} 99 | }; 100 | }); 101 | 102 | // @ts-expect-error: Missing prop1 103 | useStyles(); 104 | 105 | // @ts-expect-error: Missing prop1 106 | useStyles({}); 107 | 108 | const { classes, css, cx } = useStyles({ 109 | "prop1": { 110 | "_brand_prop1": true 111 | } 112 | }); 113 | 114 | assert>>(); 115 | assert>(); 116 | assert>(); 117 | } 118 | 119 | { 120 | const useStyles = tssMui.create({}); 121 | 122 | useStyles({ 123 | "classesOverrides": {}, 124 | "muiStyleOverridesParams": { 125 | "ownerState": { "isOn": true }, 126 | "props": { 127 | "lightBulbBorderColor": "yellow" 128 | } 129 | } 130 | }); 131 | 132 | const { css, cx, theme, ...rest } = useStyles(); 133 | 134 | assert>(); 135 | } 136 | 137 | tss.create(({ contextProp1, contextProp2, ...rest }) => { 138 | assert>(); 139 | 140 | return {}; 141 | }); 142 | -------------------------------------------------------------------------------- /src/test/types/withStyle_htmlBuiltins.tsx: -------------------------------------------------------------------------------- 1 | import { createWithStyles } from "../../withStyles"; 2 | import { assert } from "tsafe/assert"; 3 | import type { Equals } from "tsafe"; 4 | import type { ClassAttributes, HTMLAttributes } from "react"; 5 | import type { ReactHTML } from "../../tools/ReactHTML"; 6 | 7 | const theme = { 8 | "primaryColor": "blue" 9 | } as const; 10 | 11 | type Theme = typeof theme; 12 | 13 | const { withStyles } = createWithStyles({ 14 | "useTheme": () => theme 15 | }); 16 | 17 | type Props = ClassAttributes & HTMLAttributes; 18 | 19 | { 20 | const DivStyled = withStyles("div", (theme, props) => { 21 | assert>(); 22 | assert>(); 23 | 24 | return { 25 | "root": { 26 | "backgroundColor": "red" 27 | } 28 | }; 29 | }); 30 | 31 | assert>(); 32 | } 33 | 34 | withStyles("div", { 35 | "root": { 36 | "position": "absolute" 37 | } 38 | }); 39 | 40 | withStyles("div", { 41 | "root": { 42 | //@ts-expect-error 43 | "position": "absoluteXXX" 44 | } 45 | }); 46 | 47 | //We wish it wouldn't pass 48 | withStyles("div", { 49 | "root": { 50 | "position": "absolute" 51 | }, 52 | "not_root": {} 53 | }); 54 | 55 | withStyles("div", { 56 | "root": { 57 | //@ts-expect-error 58 | "color": 33 59 | } 60 | }); 61 | 62 | withStyles("div", { 63 | "root": { 64 | "position": "absolute" 65 | }, 66 | "@media (min-width: 960px)": { 67 | "root": { 68 | "position": "absolute" 69 | } 70 | } 71 | }); 72 | 73 | withStyles("div", { 74 | "root": { 75 | "position": "absolute" 76 | }, 77 | "@media (min-width: 960px)": { 78 | "root": { 79 | //@ts-expect-error 80 | "position": "absoluteXXXX" 81 | } 82 | } 83 | }); 84 | 85 | withStyles("div", { 86 | "root": { 87 | "position": "absolute" 88 | }, 89 | "@media (min-width: 960px)": { 90 | "root": { 91 | //@ts-expect-error 92 | "color": 33 93 | } 94 | } 95 | }); 96 | 97 | withStyles("div", { 98 | "root": { 99 | //@ts-expect-error: very strange that error appears here if 100 | //we dont use as const for the media query 101 | "color": "red" 102 | }, 103 | [`@media (min-width: ${960}px)`]: { 104 | "root": { 105 | "color": "red" 106 | } 107 | } 108 | }); 109 | 110 | withStyles("div", { 111 | "root": { 112 | "color": "red" 113 | }, 114 | [`@media (min-width: ${960}px)` as const]: { 115 | "root": { 116 | "position": "absolute" 117 | } 118 | } 119 | }); 120 | 121 | withStyles("div", { 122 | "root": { 123 | "color": "red" 124 | }, 125 | [`@media (min-width: ${960}px)` as const]: { 126 | "root": { 127 | //@ts-expect-error 128 | "position": "absoluteXXX" 129 | } 130 | } 131 | }); 132 | 133 | withStyles("div", { 134 | "root": { 135 | "position": "absolute" 136 | }, 137 | [`@media (min-width: ${960}px)` as const]: { 138 | "root": { 139 | //@ts-expect-error 140 | "color": 33 141 | } 142 | } 143 | }); 144 | 145 | withStyles("div", () => ({ 146 | "root": { 147 | "position": "absolute" 148 | } 149 | })); 150 | 151 | withStyles( 152 | "div", 153 | //@ts-expect-error 154 | () => ({ 155 | "root": { 156 | "position": "absoluteXXX" 157 | } 158 | }) 159 | ); 160 | 161 | withStyles( 162 | "div", 163 | //@ts-expect-error 164 | () => ({ 165 | "root": { 166 | "color": 33 167 | } 168 | }) 169 | ); 170 | 171 | withStyles("div", () => ({ 172 | "root": { 173 | "position": "absolute" 174 | }, 175 | //Unfortunately passes 😞 176 | "not_root": {} 177 | })); 178 | 179 | withStyles("div", () => ({ 180 | "root": { 181 | "position": "absolute" 182 | }, 183 | "@media (min-width: 960px)": { 184 | "root": { 185 | "position": "absolute" 186 | } 187 | } 188 | })); 189 | 190 | withStyles( 191 | "div", 192 | //@ts-expect-error 193 | () => ({ 194 | "root": { 195 | "position": "absolute" 196 | }, 197 | "@media (min-width: 960px)": { 198 | "root": { 199 | "position": "absoluteXXX" 200 | } 201 | } 202 | }) 203 | ); 204 | 205 | withStyles( 206 | "div", 207 | //@ts-expect-error 208 | () => ({ 209 | "root": { 210 | "position": "absolute" 211 | }, 212 | "@media (min-width: 960px)": { 213 | "root": { 214 | "color": 33 215 | } 216 | } 217 | }) 218 | ); 219 | 220 | withStyles( 221 | "div", 222 | //@ts-expect-error: need const 223 | () => ({ 224 | "root": { 225 | "position": "absolute" 226 | }, 227 | [`@media (min-width: ${960}px)`]: { 228 | "root": { 229 | "color": "red" 230 | } 231 | } 232 | }) 233 | ); 234 | 235 | withStyles("div", () => ({ 236 | "root": { 237 | "position": "absolute" 238 | }, 239 | [`@media (min-width: ${960}px)` as const]: { 240 | "root": { 241 | "color": "red" 242 | } 243 | } 244 | })); 245 | -------------------------------------------------------------------------------- /src/test/types/withStyles_className.tsx: -------------------------------------------------------------------------------- 1 | import { createWithStyles } from "../../withStyles"; 2 | import { assert } from "tsafe/assert"; 3 | import type { Equals } from "tsafe"; 4 | import React from "react"; 5 | 6 | const theme = { 7 | "primaryColor": "blue" 8 | } as const; 9 | 10 | type Theme = typeof theme; 11 | 12 | const { withStyles } = createWithStyles({ 13 | "useTheme": () => theme 14 | }); 15 | 16 | type Props = { 17 | className?: string; 18 | }; 19 | 20 | function MyComponent(_props: Props) { 21 | return
; 22 | } 23 | 24 | { 25 | const MyComponentStyled = withStyles( 26 | MyComponent, 27 | (theme, props, classes) => { 28 | assert>(); 29 | assert>(); 30 | assert>>(); 31 | 32 | return { 33 | "root": { 34 | "backgroundColor": "red" 35 | } 36 | }; 37 | } 38 | ); 39 | 40 | assert>(); 41 | } 42 | 43 | { 44 | type Props = { 45 | className?: string; 46 | message: string; 47 | }; 48 | 49 | class MyClassBasedComponent extends React.Component< 50 | Props, 51 | { count: number } 52 | > { 53 | render() { 54 | return ( 55 |
56 | {" "} 57 | {this.props.message} {this.state.count}{" "} 58 |
59 | ); 60 | } 61 | } 62 | 63 | const MyClassBasedComponentStyled = withStyles( 64 | MyClassBasedComponent, 65 | (theme, props, classes) => { 66 | assert>(); 67 | assert>(); 68 | assert>>(); 69 | 70 | return { 71 | "root": { 72 | "backgroundColor": "red" 73 | } 74 | }; 75 | } 76 | ); 77 | 78 | assert< 79 | Equals 80 | >(); 81 | } 82 | 83 | withStyles(MyComponent, { 84 | "root": { 85 | "position": "absolute" 86 | } 87 | }); 88 | 89 | withStyles(MyComponent, { 90 | "root": { 91 | //@ts-expect-error 92 | "position": "absoluteXXX" 93 | } 94 | }); 95 | 96 | //We wish it wouldn't pass 97 | withStyles(MyComponent, { 98 | "root": { 99 | "position": "absolute" 100 | }, 101 | "not_root": {} 102 | }); 103 | 104 | withStyles(MyComponent, { 105 | "root": { 106 | //@ts-expect-error 107 | "color": 33 108 | } 109 | }); 110 | 111 | withStyles(MyComponent, { 112 | "root": { 113 | "position": "absolute" 114 | }, 115 | "@media (min-width: 960px)": { 116 | "root": { 117 | "position": "absolute" 118 | } 119 | } 120 | }); 121 | 122 | withStyles(MyComponent, { 123 | "root": { 124 | "position": "absolute" 125 | }, 126 | "@media (min-width: 960px)": { 127 | "root": { 128 | //@ts-expect-error 129 | "position": "absoluteXXXX" 130 | } 131 | } 132 | }); 133 | 134 | withStyles(MyComponent, { 135 | "root": { 136 | "position": "absolute" 137 | }, 138 | "@media (min-width: 960px)": { 139 | "root": { 140 | //@ts-expect-error 141 | "color": 33 142 | } 143 | } 144 | }); 145 | 146 | withStyles(MyComponent, { 147 | "root": { 148 | //@ts-expect-error: very strange that error appears here if 149 | //we dont use as const for the media query 150 | "color": "red" 151 | }, 152 | [`@media (min-width: ${960}px)`]: { 153 | "root": { 154 | "color": "red" 155 | } 156 | } 157 | }); 158 | 159 | withStyles(MyComponent, { 160 | "root": { 161 | "color": "red" 162 | }, 163 | [`@media (min-width: ${960}px)` as const]: { 164 | "root": { 165 | "position": "absolute" 166 | } 167 | } 168 | }); 169 | 170 | withStyles(MyComponent, { 171 | "root": { 172 | "color": "red" 173 | }, 174 | [`@media (min-width: ${960}px)` as const]: { 175 | "root": { 176 | //@ts-expect-error 177 | "position": "absoluteXXX" 178 | } 179 | } 180 | }); 181 | 182 | withStyles(MyComponent, { 183 | "root": { 184 | "position": "absolute" 185 | }, 186 | [`@media (min-width: ${960}px)` as const]: { 187 | "root": { 188 | //@ts-expect-error 189 | "color": 33 190 | } 191 | } 192 | }); 193 | 194 | withStyles(MyComponent, () => ({ 195 | "root": { 196 | "position": "absolute" 197 | } 198 | })); 199 | 200 | withStyles( 201 | MyComponent, 202 | //@ts-expect-error 203 | () => ({ 204 | "root": { 205 | "position": "absoluteXXX" 206 | } 207 | }) 208 | ); 209 | 210 | withStyles( 211 | MyComponent, 212 | //@ts-expect-error 213 | () => ({ 214 | "root": { 215 | "color": 33 216 | } 217 | }) 218 | ); 219 | 220 | withStyles(MyComponent, () => ({ 221 | "root": { 222 | "position": "absolute" 223 | }, 224 | //Unfortunately passes 😞 225 | "not_root": {} 226 | })); 227 | 228 | withStyles(MyComponent, () => ({ 229 | "root": { 230 | "position": "absolute" 231 | }, 232 | "@media (min-width: 960px)": { 233 | "root": { 234 | "position": "absolute" 235 | } 236 | } 237 | })); 238 | 239 | withStyles( 240 | MyComponent, 241 | //@ts-expect-error 242 | () => ({ 243 | "root": { 244 | "position": "absolute" 245 | }, 246 | "@media (min-width: 960px)": { 247 | "root": { 248 | "position": "absoluteXXX" 249 | } 250 | } 251 | }) 252 | ); 253 | 254 | withStyles( 255 | MyComponent, 256 | //@ts-expect-error 257 | () => ({ 258 | "root": { 259 | "position": "absolute" 260 | }, 261 | "@media (min-width: 960px)": { 262 | "root": { 263 | "color": 33 264 | } 265 | } 266 | }) 267 | ); 268 | 269 | withStyles( 270 | MyComponent, 271 | //@ts-expect-error: need const 272 | () => ({ 273 | "root": { 274 | "position": "absolute" 275 | }, 276 | [`@media (min-width: ${960}px)`]: { 277 | "root": { 278 | "color": "red" 279 | } 280 | } 281 | }) 282 | ); 283 | 284 | withStyles(MyComponent, () => ({ 285 | "root": { 286 | "position": "absolute" 287 | }, 288 | [`@media (min-width: ${960}px)` as const]: { 289 | "root": { 290 | "color": "red" 291 | } 292 | } 293 | })); 294 | -------------------------------------------------------------------------------- /src/test/types/withStyles_classes.tsx: -------------------------------------------------------------------------------- 1 | import { createWithStyles } from "../../withStyles"; 2 | import { assert } from "tsafe/assert"; 3 | import type { Equals } from "tsafe"; 4 | import MuiButton from "@mui/material/Button"; 5 | import type { ButtonProps, ButtonClassKey } from "@mui/material/Button"; 6 | import React from "react"; 7 | 8 | const theme = { 9 | "primaryColor": "blue" 10 | } as const; 11 | 12 | type Theme = typeof theme; 13 | 14 | const { withStyles } = createWithStyles({ 15 | "useTheme": () => theme 16 | }); 17 | 18 | { 19 | { 20 | const MyComponentStyled = withStyles( 21 | MuiButton, 22 | (theme, props, classes) => { 23 | assert>(); 24 | assert>(); 25 | assert< 26 | Equals> 27 | >(); 28 | 29 | return { 30 | "colorInherit": { 31 | "position": "absolute" as const 32 | } 33 | }; 34 | } 35 | ); 36 | 37 | assert>(); 38 | } 39 | 40 | { 41 | type Props = { 42 | className?: string; 43 | classes?: { 44 | foo: string; 45 | bar: string; 46 | baz: string; 47 | }; 48 | message: string; 49 | }; 50 | 51 | class MyClassBasedComponent extends React.Component< 52 | Props, 53 | { count: number } 54 | > { 55 | render() { 56 | return ( 57 |
58 | {" "} 59 | {this.props.message} {this.state.count}{" "} 60 |
61 | ); 62 | } 63 | } 64 | 65 | const MyClassBasedComponentStyled = withStyles( 66 | MyClassBasedComponent, 67 | (theme, props, classes) => { 68 | assert>(); 69 | assert>(); 70 | assert< 71 | Equals< 72 | typeof classes, 73 | Record, string> 74 | > 75 | >(); 76 | 77 | return { 78 | "root": { 79 | "backgroundColor": "red" 80 | } 81 | }; 82 | } 83 | ); 84 | 85 | assert< 86 | Equals< 87 | typeof MyClassBasedComponent, 88 | typeof MyClassBasedComponentStyled 89 | > 90 | >(); 91 | } 92 | 93 | withStyles(MuiButton, {}); 94 | 95 | withStyles(MuiButton, { 96 | "colorInherit": { 97 | "position": "absolute" 98 | } 99 | }); 100 | 101 | withStyles(MuiButton, { 102 | "colorInherit": { 103 | //@ts-expect-error 104 | "color": 33 105 | } 106 | }); 107 | 108 | withStyles(MuiButton, { 109 | "colorInherit": { 110 | //@ts-expect-error 111 | "position": "absoluteXXX" 112 | } 113 | }); 114 | 115 | withStyles(MuiButton, { 116 | "colorInherit": { 117 | "position": "absolute" 118 | }, 119 | //@ts-expect-error 120 | "ddd": {} 121 | }); 122 | 123 | withStyles(MuiButton, { 124 | "colorInherit": { 125 | "position": "absolute" 126 | }, 127 | "@media (min-width: 960px)": { 128 | "colorInherit": { 129 | "position": "absolute" 130 | } 131 | } 132 | }); 133 | 134 | withStyles(MuiButton, { 135 | "colorInherit": { 136 | "position": "absolute" 137 | }, 138 | "@media (min-width: 960px)": { 139 | //@ts-expect-error 140 | "xxx": {} 141 | } 142 | }); 143 | 144 | withStyles(MuiButton, { 145 | "colorInherit": { 146 | "position": "absolute" 147 | }, 148 | "@media (min-width: 960px)": { 149 | "colorInherit": { 150 | //@ts-expect-error 151 | "position": "absoluteXXX" 152 | } 153 | } 154 | }); 155 | 156 | withStyles(MuiButton, { 157 | "colorInherit": { 158 | "position": "absolute" 159 | }, 160 | "@media (min-width: 960px)": { 161 | "colorInherit": { 162 | //@ts-expect-error 163 | "color": 33 164 | } 165 | } 166 | }); 167 | 168 | withStyles( 169 | MuiButton, 170 | //@ts-expect-error: needs as const 171 | () => ({ 172 | "colorInherit": { 173 | "position": "absolute" 174 | } 175 | }) 176 | ); 177 | 178 | withStyles(MuiButton, () => ({ 179 | "colorInherit": { 180 | "position": "absolute" as const 181 | } 182 | })); 183 | 184 | withStyles(MuiButton, () => ({ 185 | "colorInherit": { 186 | "position": "absolute" 187 | } as const 188 | })); 189 | 190 | withStyles( 191 | MuiButton, 192 | () => 193 | ({ 194 | "colorInherit": { 195 | "position": "absolute" 196 | } 197 | } as const) 198 | ); 199 | 200 | withStyles( 201 | MuiButton, 202 | //@ts-expect-error 203 | () => ({ 204 | "colorInherit": { 205 | "color": 33 206 | } 207 | }) 208 | ); 209 | 210 | withStyles(MuiButton, () => ({ 211 | "colorInherit": { 212 | "position": "absolute" as const 213 | }, 214 | //Unfortunately passes 215 | "ddd": {} 216 | })); 217 | 218 | withStyles( 219 | MuiButton, 220 | //@ts-expect-error: need a const 221 | () => ({ 222 | "colorInherit": { 223 | "position": "absolute" as const 224 | }, 225 | "@media (min-width: 960px)": { 226 | "colorInherit": { 227 | "position": "absolute" 228 | } 229 | } 230 | }) 231 | ); 232 | 233 | withStyles( 234 | MuiButton, 235 | //@ts-expect-error 236 | () => ({ 237 | "colorInherit": { 238 | "position": "absolute" as const 239 | }, 240 | "@media (min-width: 960px)": { 241 | "colorInherit": { 242 | "color": 33 243 | } 244 | } 245 | }) 246 | ); 247 | 248 | withStyles(MuiButton, () => ({ 249 | "colorInherit": { 250 | "position": "absolute" as const 251 | }, 252 | "@media (min-width: 960px)": { 253 | "colorInherit": { 254 | "position": "absolute" as const 255 | } 256 | } 257 | })); 258 | 259 | withStyles( 260 | MuiButton, 261 | () => 262 | ({ 263 | "colorInherit": { 264 | "position": "absolute" 265 | }, 266 | "@media (min-width: 960px)": { 267 | "colorInherit": { 268 | "position": "absolute" 269 | } 270 | } 271 | } as const) 272 | ); 273 | 274 | withStyles(MuiButton, () => ({ 275 | "colorInherit": { 276 | "position": "absolute" as const 277 | }, 278 | [`@media (min-width: ${960}px)`]: { 279 | "colorInherit": { 280 | "position": "absolute" as const 281 | } 282 | } 283 | })); 284 | 285 | withStyles( 286 | MuiButton, 287 | //@ts-expect-error 288 | () => ({ 289 | "colorInherit": { 290 | "position": "absolute" as const 291 | }, 292 | [`@media (min-width: ${960}px)`]: { 293 | "colorInherit": { 294 | "color": 33 295 | } 296 | } 297 | }) 298 | ); 299 | } 300 | -------------------------------------------------------------------------------- /src/tools/ReactComponent.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, ComponentClass } from "react"; 2 | 3 | export type ReactComponent = 4 | | ((props: Props) => ReturnType) 5 | | ComponentClass; 6 | -------------------------------------------------------------------------------- /src/tools/ReactHTML.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from "react"; 2 | 3 | // If not available (React 19+), fallback to this: 4 | export type ReactHTML = { 5 | [K in keyof JSXIntrinsicElements]: ( 6 | props: JSXIntrinsicElements[K] 7 | ) => React.ReactElement; 8 | }; 9 | 10 | export interface JSXIntrinsicElements { 11 | // HTML 12 | a: React.DetailedHTMLProps< 13 | React.AnchorHTMLAttributes, 14 | HTMLAnchorElement 15 | >; 16 | abbr: React.DetailedHTMLProps< 17 | React.HTMLAttributes, 18 | HTMLElement 19 | >; 20 | address: React.DetailedHTMLProps< 21 | React.HTMLAttributes, 22 | HTMLElement 23 | >; 24 | area: React.DetailedHTMLProps< 25 | React.AreaHTMLAttributes, 26 | HTMLAreaElement 27 | >; 28 | article: React.DetailedHTMLProps< 29 | React.HTMLAttributes, 30 | HTMLElement 31 | >; 32 | aside: React.DetailedHTMLProps< 33 | React.HTMLAttributes, 34 | HTMLElement 35 | >; 36 | audio: React.DetailedHTMLProps< 37 | React.AudioHTMLAttributes, 38 | HTMLAudioElement 39 | >; 40 | b: React.DetailedHTMLProps, HTMLElement>; 41 | base: React.DetailedHTMLProps< 42 | React.BaseHTMLAttributes, 43 | HTMLBaseElement 44 | >; 45 | bdi: React.DetailedHTMLProps< 46 | React.HTMLAttributes, 47 | HTMLElement 48 | >; 49 | bdo: React.DetailedHTMLProps< 50 | React.HTMLAttributes, 51 | HTMLElement 52 | >; 53 | big: React.DetailedHTMLProps< 54 | React.HTMLAttributes, 55 | HTMLElement 56 | >; 57 | blockquote: React.DetailedHTMLProps< 58 | React.BlockquoteHTMLAttributes, 59 | HTMLQuoteElement 60 | >; 61 | body: React.DetailedHTMLProps< 62 | React.HTMLAttributes, 63 | HTMLBodyElement 64 | >; 65 | br: React.DetailedHTMLProps< 66 | React.HTMLAttributes, 67 | HTMLBRElement 68 | >; 69 | button: React.DetailedHTMLProps< 70 | React.ButtonHTMLAttributes, 71 | HTMLButtonElement 72 | >; 73 | canvas: React.DetailedHTMLProps< 74 | React.CanvasHTMLAttributes, 75 | HTMLCanvasElement 76 | >; 77 | caption: React.DetailedHTMLProps< 78 | React.HTMLAttributes, 79 | HTMLElement 80 | >; 81 | cite: React.DetailedHTMLProps< 82 | React.HTMLAttributes, 83 | HTMLElement 84 | >; 85 | code: React.DetailedHTMLProps< 86 | React.HTMLAttributes, 87 | HTMLElement 88 | >; 89 | col: React.DetailedHTMLProps< 90 | React.ColHTMLAttributes, 91 | HTMLTableColElement 92 | >; 93 | colgroup: React.DetailedHTMLProps< 94 | React.ColgroupHTMLAttributes, 95 | HTMLTableColElement 96 | >; 97 | data: React.DetailedHTMLProps< 98 | React.DataHTMLAttributes, 99 | HTMLDataElement 100 | >; 101 | datalist: React.DetailedHTMLProps< 102 | React.HTMLAttributes, 103 | HTMLDataListElement 104 | >; 105 | dd: React.DetailedHTMLProps, HTMLElement>; 106 | del: React.DetailedHTMLProps< 107 | React.DelHTMLAttributes, 108 | HTMLModElement 109 | >; 110 | details: React.DetailedHTMLProps< 111 | React.DetailsHTMLAttributes, 112 | HTMLDetailsElement 113 | >; 114 | dfn: React.DetailedHTMLProps< 115 | React.HTMLAttributes, 116 | HTMLElement 117 | >; 118 | dialog: React.DetailedHTMLProps< 119 | React.DialogHTMLAttributes, 120 | HTMLDialogElement 121 | >; 122 | div: React.DetailedHTMLProps< 123 | React.HTMLAttributes, 124 | HTMLDivElement 125 | >; 126 | dl: React.DetailedHTMLProps< 127 | React.HTMLAttributes, 128 | HTMLDListElement 129 | >; 130 | dt: React.DetailedHTMLProps, HTMLElement>; 131 | em: React.DetailedHTMLProps, HTMLElement>; 132 | embed: React.DetailedHTMLProps< 133 | React.EmbedHTMLAttributes, 134 | HTMLEmbedElement 135 | >; 136 | fieldset: React.DetailedHTMLProps< 137 | React.FieldsetHTMLAttributes, 138 | HTMLFieldSetElement 139 | >; 140 | figcaption: React.DetailedHTMLProps< 141 | React.HTMLAttributes, 142 | HTMLElement 143 | >; 144 | figure: React.DetailedHTMLProps< 145 | React.HTMLAttributes, 146 | HTMLElement 147 | >; 148 | footer: React.DetailedHTMLProps< 149 | React.HTMLAttributes, 150 | HTMLElement 151 | >; 152 | form: React.DetailedHTMLProps< 153 | React.FormHTMLAttributes, 154 | HTMLFormElement 155 | >; 156 | h1: React.DetailedHTMLProps< 157 | React.HTMLAttributes, 158 | HTMLHeadingElement 159 | >; 160 | h2: React.DetailedHTMLProps< 161 | React.HTMLAttributes, 162 | HTMLHeadingElement 163 | >; 164 | h3: React.DetailedHTMLProps< 165 | React.HTMLAttributes, 166 | HTMLHeadingElement 167 | >; 168 | h4: React.DetailedHTMLProps< 169 | React.HTMLAttributes, 170 | HTMLHeadingElement 171 | >; 172 | h5: React.DetailedHTMLProps< 173 | React.HTMLAttributes, 174 | HTMLHeadingElement 175 | >; 176 | h6: React.DetailedHTMLProps< 177 | React.HTMLAttributes, 178 | HTMLHeadingElement 179 | >; 180 | head: React.DetailedHTMLProps< 181 | React.HTMLAttributes, 182 | HTMLHeadElement 183 | >; 184 | header: React.DetailedHTMLProps< 185 | React.HTMLAttributes, 186 | HTMLElement 187 | >; 188 | hgroup: React.DetailedHTMLProps< 189 | React.HTMLAttributes, 190 | HTMLElement 191 | >; 192 | hr: React.DetailedHTMLProps< 193 | React.HTMLAttributes, 194 | HTMLHRElement 195 | >; 196 | html: React.DetailedHTMLProps< 197 | React.HtmlHTMLAttributes, 198 | HTMLHtmlElement 199 | >; 200 | i: React.DetailedHTMLProps, HTMLElement>; 201 | iframe: React.DetailedHTMLProps< 202 | React.IframeHTMLAttributes, 203 | HTMLIFrameElement 204 | >; 205 | img: React.DetailedHTMLProps< 206 | React.ImgHTMLAttributes, 207 | HTMLImageElement 208 | >; 209 | input: React.DetailedHTMLProps< 210 | React.InputHTMLAttributes, 211 | HTMLInputElement 212 | >; 213 | ins: React.DetailedHTMLProps< 214 | React.InsHTMLAttributes, 215 | HTMLModElement 216 | >; 217 | kbd: React.DetailedHTMLProps< 218 | React.HTMLAttributes, 219 | HTMLElement 220 | >; 221 | keygen: React.DetailedHTMLProps< 222 | React.KeygenHTMLAttributes, 223 | HTMLElement 224 | >; 225 | label: React.DetailedHTMLProps< 226 | React.LabelHTMLAttributes, 227 | HTMLLabelElement 228 | >; 229 | legend: React.DetailedHTMLProps< 230 | React.HTMLAttributes, 231 | HTMLLegendElement 232 | >; 233 | li: React.DetailedHTMLProps< 234 | React.LiHTMLAttributes, 235 | HTMLLIElement 236 | >; 237 | link: React.DetailedHTMLProps< 238 | React.LinkHTMLAttributes, 239 | HTMLLinkElement 240 | >; 241 | main: React.DetailedHTMLProps< 242 | React.HTMLAttributes, 243 | HTMLElement 244 | >; 245 | map: React.DetailedHTMLProps< 246 | React.MapHTMLAttributes, 247 | HTMLMapElement 248 | >; 249 | mark: React.DetailedHTMLProps< 250 | React.HTMLAttributes, 251 | HTMLElement 252 | >; 253 | menu: React.DetailedHTMLProps< 254 | React.MenuHTMLAttributes, 255 | HTMLElement 256 | >; 257 | menuitem: React.DetailedHTMLProps< 258 | React.HTMLAttributes, 259 | HTMLElement 260 | >; 261 | meta: React.DetailedHTMLProps< 262 | React.MetaHTMLAttributes, 263 | HTMLMetaElement 264 | >; 265 | meter: React.DetailedHTMLProps< 266 | React.MeterHTMLAttributes, 267 | HTMLMeterElement 268 | >; 269 | nav: React.DetailedHTMLProps< 270 | React.HTMLAttributes, 271 | HTMLElement 272 | >; 273 | noindex: React.DetailedHTMLProps< 274 | React.HTMLAttributes, 275 | HTMLElement 276 | >; 277 | noscript: React.DetailedHTMLProps< 278 | React.HTMLAttributes, 279 | HTMLElement 280 | >; 281 | object: React.DetailedHTMLProps< 282 | React.ObjectHTMLAttributes, 283 | HTMLObjectElement 284 | >; 285 | ol: React.DetailedHTMLProps< 286 | React.OlHTMLAttributes, 287 | HTMLOListElement 288 | >; 289 | optgroup: React.DetailedHTMLProps< 290 | React.OptgroupHTMLAttributes, 291 | HTMLOptGroupElement 292 | >; 293 | option: React.DetailedHTMLProps< 294 | React.OptionHTMLAttributes, 295 | HTMLOptionElement 296 | >; 297 | output: React.DetailedHTMLProps< 298 | React.OutputHTMLAttributes, 299 | HTMLOutputElement 300 | >; 301 | p: React.DetailedHTMLProps< 302 | React.HTMLAttributes, 303 | HTMLParagraphElement 304 | >; 305 | param: React.DetailedHTMLProps< 306 | React.ParamHTMLAttributes, 307 | HTMLParamElement 308 | >; 309 | picture: React.DetailedHTMLProps< 310 | React.HTMLAttributes, 311 | HTMLElement 312 | >; 313 | pre: React.DetailedHTMLProps< 314 | React.HTMLAttributes, 315 | HTMLPreElement 316 | >; 317 | progress: React.DetailedHTMLProps< 318 | React.ProgressHTMLAttributes, 319 | HTMLProgressElement 320 | >; 321 | q: React.DetailedHTMLProps< 322 | React.QuoteHTMLAttributes, 323 | HTMLQuoteElement 324 | >; 325 | rp: React.DetailedHTMLProps, HTMLElement>; 326 | rt: React.DetailedHTMLProps, HTMLElement>; 327 | ruby: React.DetailedHTMLProps< 328 | React.HTMLAttributes, 329 | HTMLElement 330 | >; 331 | s: React.DetailedHTMLProps, HTMLElement>; 332 | samp: React.DetailedHTMLProps< 333 | React.HTMLAttributes, 334 | HTMLElement 335 | >; 336 | slot: React.DetailedHTMLProps< 337 | React.SlotHTMLAttributes, 338 | HTMLSlotElement 339 | >; 340 | script: React.DetailedHTMLProps< 341 | React.ScriptHTMLAttributes, 342 | HTMLScriptElement 343 | >; 344 | section: React.DetailedHTMLProps< 345 | React.HTMLAttributes, 346 | HTMLElement 347 | >; 348 | select: React.DetailedHTMLProps< 349 | React.SelectHTMLAttributes, 350 | HTMLSelectElement 351 | >; 352 | small: React.DetailedHTMLProps< 353 | React.HTMLAttributes, 354 | HTMLElement 355 | >; 356 | source: React.DetailedHTMLProps< 357 | React.SourceHTMLAttributes, 358 | HTMLSourceElement 359 | >; 360 | span: React.DetailedHTMLProps< 361 | React.HTMLAttributes, 362 | HTMLSpanElement 363 | >; 364 | strong: React.DetailedHTMLProps< 365 | React.HTMLAttributes, 366 | HTMLElement 367 | >; 368 | style: React.DetailedHTMLProps< 369 | React.StyleHTMLAttributes, 370 | HTMLStyleElement 371 | >; 372 | sub: React.DetailedHTMLProps< 373 | React.HTMLAttributes, 374 | HTMLElement 375 | >; 376 | summary: React.DetailedHTMLProps< 377 | React.HTMLAttributes, 378 | HTMLElement 379 | >; 380 | sup: React.DetailedHTMLProps< 381 | React.HTMLAttributes, 382 | HTMLElement 383 | >; 384 | table: React.DetailedHTMLProps< 385 | React.TableHTMLAttributes, 386 | HTMLTableElement 387 | >; 388 | template: React.DetailedHTMLProps< 389 | React.HTMLAttributes, 390 | HTMLTemplateElement 391 | >; 392 | tbody: React.DetailedHTMLProps< 393 | React.HTMLAttributes, 394 | HTMLTableSectionElement 395 | >; 396 | td: React.DetailedHTMLProps< 397 | React.TdHTMLAttributes, 398 | HTMLTableDataCellElement 399 | >; 400 | textarea: React.DetailedHTMLProps< 401 | React.TextareaHTMLAttributes, 402 | HTMLTextAreaElement 403 | >; 404 | tfoot: React.DetailedHTMLProps< 405 | React.HTMLAttributes, 406 | HTMLTableSectionElement 407 | >; 408 | th: React.DetailedHTMLProps< 409 | React.ThHTMLAttributes, 410 | HTMLTableHeaderCellElement 411 | >; 412 | thead: React.DetailedHTMLProps< 413 | React.HTMLAttributes, 414 | HTMLTableSectionElement 415 | >; 416 | time: React.DetailedHTMLProps< 417 | React.TimeHTMLAttributes, 418 | HTMLTimeElement 419 | >; 420 | title: React.DetailedHTMLProps< 421 | React.HTMLAttributes, 422 | HTMLTitleElement 423 | >; 424 | tr: React.DetailedHTMLProps< 425 | React.HTMLAttributes, 426 | HTMLTableRowElement 427 | >; 428 | track: React.DetailedHTMLProps< 429 | React.TrackHTMLAttributes, 430 | HTMLTrackElement 431 | >; 432 | u: React.DetailedHTMLProps, HTMLElement>; 433 | ul: React.DetailedHTMLProps< 434 | React.HTMLAttributes, 435 | HTMLUListElement 436 | >; 437 | "var": React.DetailedHTMLProps< 438 | React.HTMLAttributes, 439 | HTMLElement 440 | >; 441 | video: React.DetailedHTMLProps< 442 | React.VideoHTMLAttributes, 443 | HTMLVideoElement 444 | >; 445 | wbr: React.DetailedHTMLProps< 446 | React.HTMLAttributes, 447 | HTMLElement 448 | >; 449 | webview: React.DetailedHTMLProps< 450 | React.WebViewHTMLAttributes, 451 | HTMLWebViewElement 452 | >; 453 | 454 | // SVG 455 | svg: React.SVGProps; 456 | 457 | animate: React.SVGProps; // TODO: It is SVGAnimateElement but is not in TypeScript's lib.dom.d.ts for now. 458 | animateMotion: React.SVGProps; 459 | animateTransform: React.SVGProps; // TODO: It is SVGAnimateTransformElement but is not in TypeScript's lib.dom.d.ts for now. 460 | circle: React.SVGProps; 461 | clipPath: React.SVGProps; 462 | defs: React.SVGProps; 463 | desc: React.SVGProps; 464 | ellipse: React.SVGProps; 465 | feBlend: React.SVGProps; 466 | feColorMatrix: React.SVGProps; 467 | feComponentTransfer: React.SVGProps; 468 | feComposite: React.SVGProps; 469 | feConvolveMatrix: React.SVGProps; 470 | feDiffuseLighting: React.SVGProps; 471 | feDisplacementMap: React.SVGProps; 472 | feDistantLight: React.SVGProps; 473 | feDropShadow: React.SVGProps; 474 | feFlood: React.SVGProps; 475 | feFuncA: React.SVGProps; 476 | feFuncB: React.SVGProps; 477 | feFuncG: React.SVGProps; 478 | feFuncR: React.SVGProps; 479 | feGaussianBlur: React.SVGProps; 480 | feImage: React.SVGProps; 481 | feMerge: React.SVGProps; 482 | feMergeNode: React.SVGProps; 483 | feMorphology: React.SVGProps; 484 | feOffset: React.SVGProps; 485 | fePointLight: React.SVGProps; 486 | feSpecularLighting: React.SVGProps; 487 | feSpotLight: React.SVGProps; 488 | feTile: React.SVGProps; 489 | feTurbulence: React.SVGProps; 490 | filter: React.SVGProps; 491 | foreignObject: React.SVGProps; 492 | g: React.SVGProps; 493 | image: React.SVGProps; 494 | line: React.SVGProps; 495 | linearGradient: React.SVGProps; 496 | marker: React.SVGProps; 497 | mask: React.SVGProps; 498 | metadata: React.SVGProps; 499 | mpath: React.SVGProps; 500 | path: React.SVGProps; 501 | pattern: React.SVGProps; 502 | polygon: React.SVGProps; 503 | polyline: React.SVGProps; 504 | radialGradient: React.SVGProps; 505 | rect: React.SVGProps; 506 | stop: React.SVGProps; 507 | switch: React.SVGProps; 508 | symbol: React.SVGProps; 509 | text: React.SVGProps; 510 | textPath: React.SVGProps; 511 | tspan: React.SVGProps; 512 | use: React.SVGProps; 513 | view: React.SVGProps; 514 | } 515 | -------------------------------------------------------------------------------- /src/tools/assert.ts: -------------------------------------------------------------------------------- 1 | /** https://docs.tsafe.dev/assert */ 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | export function assert(condition: any, msg?: string): asserts condition { 4 | if (!condition) { 5 | throw new Error(msg); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/tools/capitalize.ts: -------------------------------------------------------------------------------- 1 | /** @see */ 2 | export function capitalize(str: S): Capitalize { 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 4 | return (str.charAt(0).toUpperCase() + str.slice(1)) as any; 5 | } 6 | -------------------------------------------------------------------------------- /src/tools/classnames.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "./assert"; 2 | import { typeGuard } from "./typeGuard"; 3 | 4 | export type CxArg = 5 | | undefined 6 | | null 7 | | string 8 | | boolean 9 | | { [className: string]: boolean | null | undefined } 10 | | readonly CxArg[]; 11 | 12 | /** Copy pasted from 13 | * https://github.com/emotion-js/emotion/blob/23f43ab9f24d44219b0b007a00f4ac681fe8712e/packages/react/src/class-names.js#L17-L63 14 | **/ 15 | export const classnames = (args: CxArg[]): string => { 16 | const len = args.length; 17 | let i = 0; 18 | let cls = ""; 19 | for (; i < len; i++) { 20 | const arg = args[i]; 21 | if (arg == null) continue; 22 | 23 | let toAdd; 24 | switch (typeof arg) { 25 | case "boolean": 26 | break; 27 | case "object": { 28 | if (Array.isArray(arg)) { 29 | toAdd = classnames(arg); 30 | } else { 31 | assert(!typeGuard<{ length: number }>(arg, false)); 32 | 33 | if ( 34 | process.env.NODE_ENV !== "production" && 35 | arg.styles !== undefined && 36 | arg.name !== undefined 37 | ) { 38 | console.error( 39 | "You have passed styles created with `css` from `@emotion/react` package to the `cx`.\n" + 40 | "`cx` is meant to compose class names (strings) so you should convert those styles to a class name by passing them to the `css` received from component." 41 | ); 42 | } 43 | toAdd = ""; 44 | for (const k in arg) { 45 | if (arg[k] && k) { 46 | toAdd && (toAdd += " "); 47 | toAdd += k; 48 | } 49 | } 50 | } 51 | break; 52 | } 53 | default: { 54 | toAdd = arg; 55 | } 56 | } 57 | if (toAdd) { 58 | cls && (cls += " "); 59 | cls += toAdd; 60 | } 61 | } 62 | return cls; 63 | }; 64 | -------------------------------------------------------------------------------- /src/tools/getDependencyArrayRef.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | /** 4 | * useEffect( 5 | * ()=> { ... }, 6 | * [ { "foo": "bar" } ] 7 | * ) 8 | * => The callback will be invoked every render. 9 | * because { "foo": "bar" } is a new instance every render. 10 | * 11 | * useEffect( 12 | * ()=> { ... }, 13 | * [ getDependencyArrayRef({ "foo": "bar" }) ] 14 | * ); 15 | * => The callback will only be invoked once. 16 | * 17 | * The optimization will be enabled only if obj is 18 | * of the form Record 19 | * otherwise the object is returned (the function is the identity function). 20 | */ 21 | export function getDependencyArrayRef(obj: any): any { 22 | if (!(obj instanceof Object) || typeof obj === "function") { 23 | return obj; 24 | } 25 | 26 | const arr: string[] = []; 27 | 28 | for (const key in obj) { 29 | const value = obj[key]; 30 | 31 | const typeofValue = typeof value; 32 | 33 | if ( 34 | !( 35 | typeofValue === "string" || 36 | (typeofValue === "number" && !isNaN(value)) || 37 | typeofValue === "boolean" || 38 | value === undefined || 39 | value === null 40 | ) 41 | ) { 42 | return obj; 43 | } 44 | 45 | arr.push(`${key}:${typeofValue}_${value}`); 46 | } 47 | 48 | return "xSqLiJdLMd9s" + arr.join("|"); 49 | } 50 | -------------------------------------------------------------------------------- /src/tools/isSSR.ts: -------------------------------------------------------------------------------- 1 | declare const jest: any; 2 | declare const mocha: any; 3 | declare const __vitest_worker__: any; 4 | 5 | export const isSSR = (() => { 6 | const isBrowser = 7 | typeof document === "object" && 8 | typeof document?.getElementById === "function"; 9 | 10 | // Check for common testing framework global variables 11 | const isJest = typeof jest !== "undefined"; 12 | const isMocha = typeof mocha !== "undefined"; 13 | const isVitest = typeof __vitest_worker__ !== "undefined"; 14 | 15 | return !isBrowser && !isJest && !isMocha && !isVitest; 16 | })(); 17 | -------------------------------------------------------------------------------- /src/tools/objectKeys.ts: -------------------------------------------------------------------------------- 1 | /** Object.keys() with types */ 2 | export function objectKeys>( 3 | o: T 4 | ): (keyof T)[] { 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | return Object.keys(o) as any; 7 | } 8 | -------------------------------------------------------------------------------- /src/tools/polyfills/Object.fromEntries.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | export const objectFromEntries: typeof Object.fromEntries = !(Object as any) 4 | .fromEntries 5 | ? (entries: any) => { 6 | if (!entries || !entries[Symbol.iterator]) { 7 | throw new Error( 8 | "Object.fromEntries() requires a single iterable argument" 9 | ); 10 | } 11 | 12 | const o: any = {}; 13 | 14 | Object.keys(entries).forEach(key => { 15 | const [k, v] = entries[key]; 16 | 17 | o[k] = v; 18 | }); 19 | 20 | return o; 21 | } 22 | : Object.fromEntries; 23 | -------------------------------------------------------------------------------- /src/tools/typeGuard.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | /** https://docs.tsafe.dev/typeguard */ 4 | export function typeGuard(_value: any, isMatched: boolean): _value is T { 5 | return isMatched; 6 | } 7 | -------------------------------------------------------------------------------- /src/tools/useGuaranteedMemo.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | 3 | /** Like react's useMemo but with guarantee that the fn 4 | * won't be invoked again if deps hasn't change */ 5 | export function useGuaranteedMemo( 6 | fn: () => T, 7 | deps: React.DependencyList 8 | ): T { 9 | const ref = useRef<{ v: T; prevDeps: unknown[] }>(); 10 | 11 | if ( 12 | !ref.current || 13 | deps.length !== ref.current.prevDeps?.length || 14 | ref.current.prevDeps.map((v, i) => v === deps[i]).indexOf(false) >= 0 15 | ) { 16 | ref.current = { 17 | "v": fn(), 18 | "prevDeps": [...deps] 19 | }; 20 | } 21 | 22 | return ref.current.v; 23 | } 24 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CSSObject as CSSObject_base, 3 | CSSInterpolation 4 | } from "@emotion/serialize"; 5 | export type { CSSInterpolation }; 6 | 7 | export interface CSSObject extends CSSObject_base { 8 | /** https://emotion.sh/docs/labels */ 9 | label?: string; 10 | } 11 | 12 | export interface Css { 13 | (template: TemplateStringsArray, ...args: CSSInterpolation[]): string; 14 | (...args: CSSInterpolation[]): string; 15 | } 16 | 17 | import type { CxArg } from "./tools/classnames"; 18 | export type { CxArg }; 19 | 20 | //SEE: https://github.com/emotion-js/emotion/pull/2276 21 | export type Cx = (...classNames: CxArg[]) => string; 22 | 23 | export function matchCSSObject( 24 | arg: TemplateStringsArray | CSSInterpolation 25 | ): arg is CSSObject { 26 | return ( 27 | arg instanceof Object && 28 | !("styles" in arg) && 29 | !("length" in arg) && 30 | !("__emotion_styles" in arg) 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/withStyles.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import React, { forwardRef, createElement } from "react"; 3 | import type { ReactHTML } from "./tools/ReactHTML"; 4 | import type { ReactComponent } from "./tools/ReactComponent"; 5 | import type { CSSObject } from "./types"; 6 | import { createMakeStyles } from "./makeStyles"; 7 | import { capitalize } from "./tools/capitalize"; 8 | import type { EmotionCache } from "@emotion/cache"; 9 | 10 | export function createWithStyles(params: { 11 | useTheme: () => Theme; 12 | cache?: EmotionCache; 13 | }) { 14 | const { useTheme, cache } = params; 15 | 16 | const { makeStyles } = createMakeStyles({ useTheme, cache }); 17 | 18 | function withStyles< 19 | C extends ReactComponent | keyof ReactHTML, 20 | Props extends C extends ReactComponent 21 | ? P 22 | : C extends keyof ReactHTML 23 | ? ReactHTML[C] extends ReactComponent 24 | ? NonNullable

25 | : never 26 | : never, 27 | CssObjectByRuleName extends Props extends { 28 | classes?: Partial; 29 | } 30 | ? { [RuleName in keyof ClassNameByRuleName]?: CSSObject } & { 31 | [mediaQuery: `@media${string}`]: { 32 | [RuleName in keyof ClassNameByRuleName]?: CSSObject; 33 | }; 34 | } 35 | : { root: CSSObject } & { 36 | [mediaQuery: `@media${string}`]: { root: CSSObject }; 37 | } 38 | >( 39 | Component: C, 40 | cssObjectByRuleNameOrGetCssObjectByRuleName: 41 | | (CssObjectByRuleName & { 42 | [mediaQuery: `@media${string}`]: { 43 | [Key in keyof CssObjectByRuleName]?: CSSObject; 44 | }; 45 | }) 46 | | (( 47 | theme: Theme, 48 | props: Props, 49 | classes: Record< 50 | Exclude, 51 | string 52 | > 53 | ) => CssObjectByRuleName), 54 | params?: { name?: string | Record; uniqId?: string } 55 | ): C extends keyof ReactHTML ? ReactHTML[C] : C { 56 | const Component_: ReactComponent = 57 | typeof Component === "string" 58 | ? (() => { 59 | const tag = Component as keyof ReactHTML; 60 | 61 | const Out = function ({ children, ...props }: any) { 62 | return createElement(tag, props, children); 63 | }; 64 | 65 | Object.defineProperty(Out, "name", { 66 | "value": capitalize(tag) 67 | }); 68 | 69 | return Out; 70 | })() 71 | : Component; 72 | 73 | /** 74 | * Get component name for wrapping 75 | * @see https://reactjs.org/docs/higher-order-components.html#convention-wrap-the-display-name-for-easy-debugging 76 | */ 77 | const name: string | undefined = (() => { 78 | { 79 | const { name: nameOrWrappedName } = params ?? {}; 80 | 81 | if (nameOrWrappedName !== undefined) { 82 | return typeof nameOrWrappedName !== "object" 83 | ? nameOrWrappedName 84 | : Object.keys(nameOrWrappedName)[0]; 85 | } 86 | } 87 | 88 | let name: string | undefined = undefined; 89 | 90 | displayName: { 91 | const displayName = (Component_ as any).displayName; 92 | 93 | if (typeof displayName !== "string" || displayName === "") { 94 | break displayName; 95 | } 96 | 97 | name = displayName; 98 | } 99 | 100 | functionName: { 101 | if (name !== undefined) { 102 | break functionName; 103 | } 104 | 105 | const functionName = Component_.name; 106 | 107 | if (typeof functionName !== "string" || functionName === "") { 108 | break functionName; 109 | } 110 | 111 | name = functionName; 112 | } 113 | 114 | if (name === undefined) { 115 | return undefined; 116 | } 117 | 118 | // Special case for dollar sign 119 | name = name.replace(/\$/g, "usd"); 120 | // Replacing open and close parentheses 121 | name = name.replace(/\(/g, "_").replace(/\)/g, "_"); 122 | // Catch-all replacement for characters not allowed in CSS class names 123 | name = name.replace(/[^a-zA-Z0-9-_]/g, "_"); 124 | 125 | return name; 126 | })(); 127 | 128 | const useStyles = makeStyles({ ...params, name })( 129 | typeof cssObjectByRuleNameOrGetCssObjectByRuleName === "function" 130 | ? (theme: Theme, props: Props, classes: Record) => 131 | incorporateMediaQueries( 132 | cssObjectByRuleNameOrGetCssObjectByRuleName( 133 | theme, 134 | props, 135 | classes 136 | ) 137 | ) as any 138 | : (incorporateMediaQueries( 139 | cssObjectByRuleNameOrGetCssObjectByRuleName 140 | ) as any) 141 | ); 142 | 143 | function getHasNonRootClasses(classes: Record) { 144 | for (const name in classes) { 145 | if (name === "root") { 146 | continue; 147 | } 148 | 149 | return true; 150 | } 151 | 152 | return false; 153 | } 154 | 155 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 156 | const Out = forwardRef(function (props, ref) { 157 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 158 | const { className, classes: _classes, ...rest } = props; 159 | 160 | const { classes, cx } = useStyles(props, { props }); 161 | 162 | const rootClassName = cx(classes.root, className); 163 | 164 | fixedClassesByClasses.set(classes, { 165 | ...classes, 166 | "root": rootClassName 167 | }); 168 | 169 | return ( 170 | 180 | ); 181 | }); 182 | 183 | if (name !== undefined) { 184 | Out.displayName = `${capitalize(name)}WithStyles`; 185 | 186 | Object.defineProperty(Out, "name", { "value": Out.displayName }); 187 | } 188 | 189 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 190 | return Out as any; 191 | } 192 | 193 | withStyles.getClasses = getClasses; 194 | 195 | return { withStyles }; 196 | } 197 | 198 | const fixedClassesByClasses = new WeakMap>(); 199 | 200 | const errorMessageGetClasses = 201 | "getClasses should only be used in conjunction with withStyles"; 202 | 203 | function getClasses(props: { 204 | className?: string; 205 | classes?: Classes; 206 | }): Classes extends Record 207 | ? Classes extends Partial> 208 | ? Record 209 | : Classes 210 | : { root: string } { 211 | const classesIn = props.classes; 212 | 213 | if (classesIn === undefined) { 214 | throw new Error(errorMessageGetClasses); 215 | } 216 | 217 | const classes = fixedClassesByClasses.get(classesIn); 218 | 219 | if (classes === undefined) { 220 | throw new Error(errorMessageGetClasses); 221 | } 222 | 223 | return classes as any; 224 | } 225 | 226 | function incorporateMediaQueries( 227 | cssObjectByRuleNameWithMediaQueries: { 228 | [RuleName_ in string]?: CSSObject; 229 | } & { 230 | [mediaQuery: `@media${string}`]: { [RuleName_ in string]?: CSSObject }; 231 | } 232 | ): { [RuleName_ in string]: CSSObject } { 233 | const cssObjectByRuleName: { [RuleName_ in string]: CSSObject } = {}; 234 | 235 | const cssObjectByRuleNameWithMediaQueriesByMediaQuery: { 236 | [mediaQuery: `@media${string}`]: { [RuleName_ in string]?: CSSObject }; 237 | } = {}; 238 | 239 | Object.keys(cssObjectByRuleNameWithMediaQueries).forEach( 240 | ruleNameOrMediaQuery => 241 | ((ruleNameOrMediaQuery.startsWith("@media") 242 | ? (cssObjectByRuleNameWithMediaQueriesByMediaQuery as any) 243 | : (cssObjectByRuleName as any))[ruleNameOrMediaQuery] = 244 | cssObjectByRuleNameWithMediaQueries[ruleNameOrMediaQuery]) 245 | ); 246 | 247 | Object.keys(cssObjectByRuleNameWithMediaQueriesByMediaQuery).forEach( 248 | mediaQuery => { 249 | const cssObjectByRuleNameBis = 250 | cssObjectByRuleNameWithMediaQueriesByMediaQuery[ 251 | mediaQuery as any 252 | ]; 253 | 254 | Object.keys(cssObjectByRuleNameBis).forEach( 255 | ruleName => 256 | (cssObjectByRuleName[ruleName] = { 257 | ...(cssObjectByRuleName[ruleName] ?? {}), 258 | [mediaQuery]: cssObjectByRuleNameBis[ruleName] 259 | }) 260 | ); 261 | } 262 | ); 263 | 264 | return cssObjectByRuleName; 265 | } 266 | -------------------------------------------------------------------------------- /src/withStyles_compat.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import React, { forwardRef, createElement } from "react"; 3 | import type { ReactHTML } from "./tools/ReactHTML"; 4 | import type { ReactComponent } from "./tools/ReactComponent"; 5 | import type { CSSObject } from "./types"; 6 | import { createMakeStyles } from "./makeStyles"; 7 | import { capitalize } from "./tools/capitalize"; 8 | import type { EmotionCache } from "@emotion/cache"; 9 | 10 | export function createWithStyles(params: { 11 | useTheme: () => Theme; 12 | cache?: EmotionCache; 13 | }) { 14 | const { useTheme, cache } = params; 15 | 16 | const { makeStyles } = createMakeStyles({ useTheme, cache }); 17 | 18 | function withStyles< 19 | C extends ReactComponent | keyof ReactHTML, 20 | Props extends C extends ReactComponent 21 | ? P 22 | : C extends keyof ReactHTML 23 | ? ReactHTML[C] extends ReactComponent 24 | ? NonNullable

25 | : never 26 | : never, 27 | CssObjectByRuleName extends Props extends { 28 | classes?: Partial; 29 | } 30 | ? { [RuleName in keyof ClassNameByRuleName]?: CSSObject } 31 | : { root: CSSObject } 32 | >( 33 | Component: C, 34 | cssObjectByRuleNameOrGetCssObjectByRuleName: 35 | | CssObjectByRuleName 36 | | (( 37 | theme: Theme, 38 | props: Props, 39 | classes: Record 40 | ) => CssObjectByRuleName), 41 | params?: { name?: string | Record; uniqId?: string } 42 | ): C extends keyof ReactHTML ? ReactHTML[C] : C { 43 | const Component_: ReactComponent = 44 | typeof Component === "string" 45 | ? (() => { 46 | const tag = Component as keyof ReactHTML; 47 | 48 | const Out = function ({ children, ...props }: any) { 49 | return createElement(tag, props, children); 50 | }; 51 | 52 | Object.defineProperty(Out, "name", { 53 | "value": capitalize(tag) 54 | }); 55 | 56 | return Out; 57 | })() 58 | : Component; 59 | 60 | /** 61 | * Get component name for wrapping 62 | * @see https://reactjs.org/docs/higher-order-components.html#convention-wrap-the-display-name-for-easy-debugging 63 | */ 64 | const name: string | undefined = (() => { 65 | { 66 | const { name: nameOrWrappedName } = params ?? {}; 67 | 68 | if (nameOrWrappedName !== undefined) { 69 | return typeof nameOrWrappedName !== "object" 70 | ? nameOrWrappedName 71 | : Object.keys(nameOrWrappedName)[0]; 72 | } 73 | } 74 | 75 | let name: string | undefined = undefined; 76 | 77 | displayName: { 78 | const displayName = (Component_ as any).displayName; 79 | 80 | if (typeof displayName !== "string" || displayName === "") { 81 | break displayName; 82 | } 83 | 84 | name = displayName; 85 | } 86 | 87 | functionName: { 88 | if (name !== undefined) { 89 | break functionName; 90 | } 91 | 92 | const functionName = Component_.name; 93 | 94 | if (typeof functionName !== "string" || functionName === "") { 95 | break functionName; 96 | } 97 | 98 | name = functionName; 99 | } 100 | 101 | if (name === undefined) { 102 | return undefined; 103 | } 104 | 105 | // Special case for dollar sign 106 | name = name.replace(/\$/g, "usd"); 107 | // Replacing open and close parentheses 108 | name = name.replace(/\(/g, "_").replace(/\)/g, "_"); 109 | // Catch-all replacement for characters not allowed in CSS class names 110 | name = name.replace(/[^a-zA-Z0-9-_]/g, "_"); 111 | 112 | return name; 113 | })(); 114 | 115 | const useStyles = makeStyles({ ...params, name })( 116 | typeof cssObjectByRuleNameOrGetCssObjectByRuleName === "function" 117 | ? (theme: Theme, props: Props, classes: Record) => 118 | incorporateMediaQueries( 119 | cssObjectByRuleNameOrGetCssObjectByRuleName( 120 | theme, 121 | props, 122 | classes 123 | ) 124 | ) as any 125 | : (incorporateMediaQueries( 126 | cssObjectByRuleNameOrGetCssObjectByRuleName 127 | ) as any) 128 | ); 129 | 130 | function getHasNonRootClasses(classes: Record) { 131 | for (const name in classes) { 132 | if (name === "root") { 133 | continue; 134 | } 135 | 136 | return true; 137 | } 138 | 139 | return false; 140 | } 141 | 142 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 143 | const Out = forwardRef(function (props, ref) { 144 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 145 | const { className, classes: _classes, ...rest } = props; 146 | 147 | const { classes, cx } = useStyles(props, { props }); 148 | 149 | const rootClassName = cx(classes.root, className); 150 | 151 | fixedClassesByClasses.set(classes, { 152 | ...classes, 153 | "root": rootClassName 154 | }); 155 | 156 | return ( 157 | 167 | ); 168 | }); 169 | 170 | if (name !== undefined) { 171 | Out.displayName = `${capitalize(name)}WithStyles`; 172 | 173 | Object.defineProperty(Out, "name", { "value": Out.displayName }); 174 | } 175 | 176 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 177 | return Out as any; 178 | } 179 | 180 | withStyles.getClasses = getClasses; 181 | 182 | return { withStyles }; 183 | } 184 | 185 | const fixedClassesByClasses = new WeakMap>(); 186 | 187 | const errorMessageGetClasses = 188 | "getClasses should only be used in conjunction with withStyles"; 189 | 190 | function getClasses(props: { 191 | className?: string; 192 | classes?: Classes; 193 | }): Classes extends Record 194 | ? Classes extends Partial> 195 | ? Record 196 | : Classes 197 | : { root: string } { 198 | const classesIn = props.classes; 199 | 200 | if (classesIn === undefined) { 201 | throw new Error(errorMessageGetClasses); 202 | } 203 | 204 | const classes = fixedClassesByClasses.get(classesIn); 205 | 206 | if (classes === undefined) { 207 | throw new Error(errorMessageGetClasses); 208 | } 209 | 210 | return classes as any; 211 | } 212 | 213 | function incorporateMediaQueries( 214 | cssObjectByRuleNameWithMediaQueries: { 215 | [RuleName_ in string]?: CSSObject; 216 | } & { 217 | [mediaQuery: `@media${string}`]: { [RuleName_ in string]?: CSSObject }; 218 | } 219 | ): { [RuleName_ in string]: CSSObject } { 220 | const cssObjectByRuleName: { [RuleName_ in string]: CSSObject } = {}; 221 | 222 | const cssObjectByRuleNameWithMediaQueriesByMediaQuery: { 223 | [mediaQuery: `@media${string}`]: { [RuleName_ in string]?: CSSObject }; 224 | } = {}; 225 | 226 | Object.keys(cssObjectByRuleNameWithMediaQueries).forEach( 227 | ruleNameOrMediaQuery => 228 | ((ruleNameOrMediaQuery.startsWith("@media") 229 | ? (cssObjectByRuleNameWithMediaQueriesByMediaQuery as any) 230 | : (cssObjectByRuleName as any))[ruleNameOrMediaQuery] = 231 | cssObjectByRuleNameWithMediaQueries[ruleNameOrMediaQuery]) 232 | ); 233 | 234 | Object.keys(cssObjectByRuleNameWithMediaQueriesByMediaQuery).forEach( 235 | mediaQuery => { 236 | const cssObjectByRuleNameBis = 237 | cssObjectByRuleNameWithMediaQueriesByMediaQuery[ 238 | mediaQuery as any 239 | ]; 240 | 241 | Object.keys(cssObjectByRuleNameBis).forEach( 242 | ruleName => 243 | (cssObjectByRuleName[ruleName] = { 244 | ...(cssObjectByRuleName[ruleName] ?? {}), 245 | [mediaQuery]: cssObjectByRuleNameBis[ruleName] 246 | }) 247 | ); 248 | } 249 | ); 250 | 251 | return cssObjectByRuleName; 252 | } 253 | -------------------------------------------------------------------------------- /tsconfig-esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "es2020", 5 | "target": "ES2018", 6 | "outDir": "./dist/esm" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES2015", 5 | "lib": ["es2015", "ES2019.Object", "DOM"], 6 | "esModuleInterop": true, 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "outDir": "./dist", 10 | "sourceMap": false, 11 | "newLine": "LF", 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "incremental": true, 15 | "strict": true, 16 | "downlevelIteration": true, 17 | "jsx": "react", 18 | "noFallthroughCasesInSwitch": true, 19 | "skipLibCheck": true 20 | }, 21 | "include": ["src"], 22 | "exclude": ["src/test/apps", "src/bin"] 23 | } 24 | --------------------------------------------------------------------------------