├── .babelrc ├── .env ├── .eslintrc.js ├── .github ├── stale.yml └── workflows │ ├── ci-tests.yml │ └── codeql-analysis.yml ├── .gitignore ├── .idea └── prettier.xml ├── .npmignore ├── .prettierrc.js ├── .storybook ├── main.js ├── preview.js ├── stories.js ├── theme.js └── ui.js ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── docs ├── common-theme-providers.md ├── emotion-logo.png ├── material-logo.svg ├── storybook-logo.png ├── styled-logo.png └── theme-panel.png ├── nodemon.json ├── package.json ├── preset.js ├── public ├── favicon.ico ├── index.html └── manifest.json ├── register.js ├── src ├── __test__ │ └── selectors.test.js ├── actions.js ├── config.js ├── helpers │ ├── createSelector.js │ ├── sampleTheme.d.ts │ └── sampleTheme.js ├── index.d.ts ├── index.js ├── manager │ ├── UI │ │ ├── Caption.js │ │ ├── Caption.styled.js │ │ ├── IconButton.js │ │ ├── Text.js │ │ ├── Toolbar.js │ │ └── Toolbar.styled.js │ ├── components │ │ ├── ColorDetails.js │ │ ├── ColorDetails.styled.js │ │ ├── SelectTheme.js │ │ ├── SelectTheme.styled.js │ │ ├── ThemeBrowser.js │ │ └── ThemeBrowser.styled.js │ ├── editors │ │ ├── ReactJsonEditor.js │ │ ├── ReactYamlEditor.js │ │ └── useEditors.js │ └── register.js ├── preset │ ├── index.js │ └── preview.js ├── preview │ ├── index.d.ts │ ├── index.js │ └── onThemeSwitch.js ├── register.js ├── selectors.js └── utils │ ├── clipboard.js │ ├── colors.js │ ├── default.js │ └── index.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current", 8 | "browsers": "defaults" 9 | } 10 | } 11 | ], 12 | "@babel/preset-react" 13 | ], 14 | "plugins": [ 15 | "@babel/plugin-proposal-class-properties" 16 | ] 17 | } -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const prettier = require('./.prettierrc.js'); 2 | const error = 2; 3 | const warn = 1; 4 | const ignore = 0; 5 | 6 | module.exports = { 7 | root: true, 8 | extends: ['eslint-config-airbnb', 'plugin:jest/recommended', 'prettier'], 9 | plugins: ['prettier', 'jest', 'react', 'json'], 10 | parser: 'babel-eslint', 11 | parserOptions: { 12 | sourceType: 'module', 13 | }, 14 | env: { 15 | browser: true, 16 | es6: true, 17 | node: true, 18 | 'jest/globals': true, 19 | }, 20 | rules: { 21 | 'react/jsx-props-no-spreading': 'off', 22 | 'arrow-body-style': 'off', 23 | strict: [error, 'never'], 24 | 'prettier/prettier': [warn, prettier], 25 | quotes: [warn, 'single', { avoidEscape: true }], 26 | 'class-methods-use-this': ignore, 27 | 'arrow-parens': [warn, 'as-needed'], 28 | 'space-before-function-paren': ignore, 29 | 'import/no-unresolved': warn, 30 | 'import/extensions': [ 31 | // because of highlight.js and fuse.js 32 | warn, 33 | { 34 | js: 'never', 35 | json: 'always', 36 | }, 37 | ], 38 | 'import/no-extraneous-dependencies': [ 39 | error, 40 | { 41 | devDependencies: [ 42 | 'examples/**', 43 | '**/example/**', 44 | '*.js', 45 | '**/*.test.js', 46 | '**/scripts/*.js', 47 | '**/stories/*.js', 48 | '**/__tests__/*.js', 49 | 'src/**', 50 | ], 51 | peerDependencies: true, 52 | }, 53 | ], 54 | 'import/prefer-default-export': ignore, 55 | 'react/prop-types': ignore, 56 | 'react/jsx-wrap-multilines': ignore, 57 | 'react/jsx-indent': ignore, 58 | 'react/jsx-indent-props': ignore, 59 | 'react/jsx-closing-bracket-location': ignore, 60 | 'react/jsx-uses-react': error, 61 | 'react/jsx-uses-vars': error, 62 | 'react/react-in-jsx-scope': error, 63 | 'react/jsx-filename-extension': [ 64 | warn, 65 | { 66 | extensions: ['.js', '.jsx'], 67 | }, 68 | ], 69 | 'jsx-a11y/accessible-emoji': ignore, 70 | 'jsx-a11y/href-no-hash': ignore, 71 | 'jsx-a11y/label-has-for': ignore, 72 | 'jsx-a11y/anchor-is-valid': ['warn', { aspects: ['invalidHref'] }], 73 | 'react/no-unescaped-entities': ignore, 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 120 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 15 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - dependencies 10 | - bug 11 | - help wanted 12 | # Label to use when marking an issue as stale 13 | staleLabel: wontfix 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | # Comment to post when closing a stale issue. Set to `false` to disable 20 | closeComment: false 21 | -------------------------------------------------------------------------------- /.github/workflows/ci-tests.yml: -------------------------------------------------------------------------------- 1 | name: ci_tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches-ignore: 9 | - gh-pages 10 | 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: 12 20 | - name: Cache node modules 21 | uses: actions/cache@v1 22 | with: 23 | path: node_modules 24 | key: dependencies 25 | - run: yarn 26 | - run: yarn lint 27 | - run: yarn test 28 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '30 13 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.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 | /dist 13 | 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | .idea 22 | 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src 2 | /public 3 | /.storybook 4 | /node_modules 5 | /docs 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | bracketSpacing: true, 4 | trailingComma: 'all', 5 | arrowParens: 'avoid', 6 | singleQuote: true, 7 | }; 8 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['./stories.js'], 3 | addons: ["../preset.js", '@storybook/addon-actions', '@storybook/addon-links', /* '@storybook/addon-backgrounds', */ 'storybook-dark-mode'] 4 | } 5 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | backgrounds: { 3 | default: 'light', 4 | values: [ 5 | { 6 | name: 'light', 7 | value: '#fff', 8 | }, 9 | { 10 | name: 'dark', 11 | value: '#444', 12 | }, 13 | ], 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.storybook/stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { ThemeProvider } from 'emotion-theming'; 4 | 5 | import { storiesOf } from '@storybook/react'; 6 | import { withThemes } from '../src/index.js'; 7 | 8 | import { ButtonSolid, ButtonRegular, Text, content } from './ui'; 9 | import { theme, themeAlt, darkTheme } from './theme'; 10 | 11 | const providerFn = ({ theme, children }) => { 12 | return {children}; 13 | }; 14 | 15 | export const onThemeSwitch = context => { 16 | const { theme } = context; 17 | const background = theme.name === 'Dark theme' ? 'pink' : 'red'; 18 | const parameters = { 19 | backgrounds: null,/* { 20 | default: background, 21 | }, */ 22 | }; 23 | return { 24 | parameters, 25 | }; 26 | }; 27 | 28 | const getCustomValueSnippet = selectedValue => `color: ${selectedValue?.value}` 29 | 30 | 31 | storiesOf('Button', module) 32 | .addDecorator( 33 | withThemes(ThemeProvider, [theme, themeAlt, darkTheme], { 34 | providerFn, 35 | onThemeSwitch, 36 | // getCustomValueSnippet 37 | } 38 | ), 39 | ) 40 | .add('Buttons1', () => Hello Button) 41 | .add('Buttons2', () => Hello Button) 42 | .add('Buttons3', () => {content}); 43 | 44 | storiesOf('Non themable', module).add('Component1', () => ( 45 | 46 | )); 47 | -------------------------------------------------------------------------------- /.storybook/theme.js: -------------------------------------------------------------------------------- 1 | const colors = { 2 | white: '#ffffff', 3 | whiteTransparent: 'rgba(255, 255, 255, 0.5)', 4 | blackTransparent: '#e8eae8', 5 | accent1: '#d2093b', 6 | accent2: '#252525', 7 | accent3: '#c2c8cb', 8 | accent4: '#044e7c', 9 | accent5: '#ac924d', 10 | accent6: '#e0a8b4', 11 | accent7: '#425550', 12 | accent8: '#8b8b6b', 13 | accent9: '#9c0935', 14 | }; 15 | 16 | const media = { 17 | md: '@media (max-width: 767px)', 18 | sm: '@media (max-width: 499px)', 19 | }; 20 | 21 | export const theme = { 22 | name: 'Light theme 1', 23 | palette: { 24 | colors, 25 | }, 26 | media, 27 | }; 28 | 29 | export const themeAlt = { 30 | ...theme, 31 | name: 'Light theme 2', 32 | palette: { 33 | colors: { 34 | white: '#ffffff', 35 | whiteTransparent: 'rgba(255, 255, 255, 0.5)', 36 | blackTransparent: '#fcfcfc', 37 | accent1: '#3bd9d6', 38 | accent2: '#0a8997', 39 | accent3: '#292b2c', 40 | accent4: '#7c0435', 41 | accent5: '#ac924d', 42 | accent6: '#e0a8b4', 43 | accent7: '#6cb09e', 44 | accent8: '#8b8b6b', 45 | accent9: '#1f595f', 46 | textRed: 'orange', 47 | }, 48 | }, 49 | }; 50 | 51 | export const darkTheme = { 52 | ...theme, 53 | name: 'Dark theme', 54 | palette: { 55 | colors: { 56 | white: '#d7d4d4', 57 | whiteTransparent: 'rgba(255, 255, 255, 0.5)', 58 | blackTransparent: '#707270', 59 | accent1: '#46496c', 60 | accent2: '#c7c7cb', 61 | accent3: '#c2c8cb', 62 | accent4: '#044e7c', 63 | accent5: '#ac924d', 64 | accent6: '#e0a8b4', 65 | accent7: '#425550', 66 | accent8: '#8b8b6b', 67 | accent9: '#9c0935', 68 | bgGrey: '#888888', 69 | textRed: '#Fd1500', 70 | }, 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /.storybook/ui.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styled from '@emotion/styled'; 4 | 5 | export const ButtonSolid = styled.button` 6 | background-color: ${({ theme }) => theme.palette.colors.accent1}; 7 | border: none; 8 | border-radius: 2px; 9 | padding: 8px 16px; 10 | margin: 8px; 11 | font-size: 16px; 12 | text-transform: uppercase; 13 | color: ${({ theme }) => theme.palette.colors.white}; 14 | min-width: 140px; 15 | min-height: 60px; 16 | `; 17 | 18 | export const ButtonRegular = styled.button` 19 | border: 2px solid ${({ theme }) => theme.palette.colors.accent1}; 20 | border-radius: 2px; 21 | background-color: ${({ theme }) => theme.palette.colors.blackTransparent}; 22 | padding: 8px 16px; 23 | margin: 8px; 24 | font-size: 16px; 25 | text-transform: uppercase; 26 | color: ${({ theme }) => theme.palette.colors.accent2}; 27 | min-width: 140px; 28 | min-height: 60px; 29 | `; 30 | 31 | export const Text = styled.div` 32 | margin: auto; 33 | max-width: 600px; 34 | color: ${({ theme }) => theme.palette.colors.accent2}; 35 | 36 | b { 37 | color: ${({ theme }) => theme.palette.colors.accent4}; 38 | } 39 | 40 | em { 41 | color: ${({ theme }) => theme.palette.colors.accent6}; 42 | background-color: ${({ theme }) => theme.palette.colors.accent9}; 43 | } 44 | 45 | i { 46 | color: ${({ theme }) => theme.palette.colors.accent3}; 47 | background-color: ${({ theme }) => theme.palette.colors.accent7}; 48 | } 49 | 50 | a { 51 | color: ${({ theme }) => theme.palette.colors.accent1}; 52 | } 53 | 54 | `; 55 | 56 | export const content = <> 57 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Aliquam sem et tortor consequat id porta nibh venenatis. Faucibus pulvinar elementum integer enim neque. Dui faucibus in ornare quam viverra orci sagittis. Cras tincidunt lobortis feugiat vivamus at augue. Posuere ac ut consequat semper viverra nam libero. Tincidunt id aliquet risus feugiat in ante metus dictum at. Nulla aliquet porttitor lacus luctus accumsan tortor posuere ac ut. Molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit. Risus nullam eget felis eget nunc lobortis mattis. Rutrum quisque non tellus orci ac auctor. At consectetur lorem donec massa sapien faucibus et. Euismod lacinia at quis risus sed vulputate. Nullam eget felis eget nunc lobortis mattis aliquam faucibus purus. Magna fermentum iaculis eu non diam.

58 | 59 |

Non quam lacus suspendisse faucibus interdum posuere lorem ipsum. Eget mi proin sed libero enim sed faucibus turpis. Leo integer malesuada nunc vel risus commodo viverra maecenas accumsan. Molestie at elementum eu facilisis. Vulputate sapien nec sagittis aliquam. Neque ornare aenean euismod elementum nisi quis. Ultrices neque ornare aenean euismod elementum. Ut eu sem integer vitae justo eget magna fermentum. Scelerisque eu ultrices vitae auctor eu augue ut. Fermentum dui faucibus in ornare quam viverra. Pharetra massa massa ultricies mi. Lorem ipsum dolor sit amet consectetur adipiscing.

60 | 61 |

Felis donec et odio pellentesque diam volutpat commodo sed egestas. Mauris in aliquam sem fringilla ut. Quam adipiscing vitae proin sagittis nisl rhoncus mattis rhoncus. Orci sagittis eu volutpat odio facilisis mauris. Nisi vitae suscipit tellus mauris a diam maecenas sed enim. Ornare suspendisse sed nisi lacus sed viverra. Sit amet est placerat in egestas erat imperdiet sed. Vel pretium lectus quam id. Id leo in vitae turpis massa sed elementum. Diam sollicitudin tempor id eu. Metus vulputate eu scelerisque felis imperdiet proin fermentum leo. Cum sociis natoque penatibus et magnis dis parturient.

62 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "statusBar.background": "#15add3", 4 | "statusBar.foreground": "#15202b", 5 | "statusBarItem.hoverBackground": "#1087a5", 6 | "titleBar.activeBackground": "#15add3", 7 | "titleBar.activeForeground": "#15202b", 8 | "titleBar.inactiveBackground": "#15add399", 9 | "titleBar.inactiveForeground": "#15202b99", 10 | "sash.hoverBorder": "#15add3", 11 | "statusBar.border": "#15add3", 12 | "statusBar.debuggingBackground": "#d33b15", 13 | "statusBar.debuggingBorder": "#d33b15", 14 | "statusBar.debuggingForeground": "#e7e7e7", 15 | "statusBarItem.remoteBackground": "#15add3", 16 | "statusBarItem.remoteForeground": "#15202b", 17 | "titleBar.border": "#15add3", 18 | "commandCenter.border": "#15202b99" 19 | }, 20 | "peacock.color": "#15add3" 21 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | usulpro@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Oleg Proskurin 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 | [![npm version](https://badge.fury.io/js/%40react-theming%2Fstorybook-addon.svg)](https://badge.fury.io/js/%40react-theming%2Fstorybook-addon) 2 | [![Storybook](https://raw.githubusercontent.com/storybookjs/storybook-addon-console/master/docs/storybook.svg?sanitize=true)](https://react-theming.github.io/storybook-addon) 3 | 4 | # Storybook Addon @ React Theming 5 | 6 | Storybook addon for Styled Components, Emotion, Material-UI and any other theming solution. Allows to develop themed components in isolation. 7 | 8 | ```shell 9 | npm i --save-dev @react-theming/storybook-addon 10 | ``` 11 | [Demo](https://react-theming.github.io/storybook-addon) 12 | 13 | ![example](https://raw.githubusercontent.com/react-theming/storybook-addon/master/docs/theme-panel.png) 14 | 15 | ## Features :dizzy: 16 | 17 | - Universal - can be used with any styling library 18 | - Switching between themes from addon panel. 19 | - Change a color and see how it affects to your components 20 | - Easily copy-paste paths of nesting theme props into your code 21 | - Auto changes background 22 | - Supports dark Storybook theme 23 | - Keep selected theme on stories updates 24 | 25 | 26 | ## Usage 27 | 28 | specify addon in `.storybook/main.js` 29 | 30 | ```js 31 | // .storybook/main.js 32 | 33 | module.exports = { 34 | stories: ['../src/**/*.stories.js'], 35 | addons: ['@react-theming/storybook-addon'], 36 | }; 37 | ``` 38 | 39 | or in `.storybook/addons.js` for older versions of Storybook 40 | 41 | ```js 42 | import '@react-theming/storybook-addon/register'; 43 | 44 | ``` 45 | 46 | Then you'll need to add a decorator with a ThemeProvider of your library. This project is not related to any particular styling solutions, instead, you can use **any of theme providers** you're using in your project. 47 | 48 | ```js 49 | import ThemeProvider from 'library-of-your-choice'; 50 | import { withThemes } from '@react-theming/storybook-addon'; 51 | import { theme } from '../src/theme'; 52 | 53 | // create decorator 54 | const themingDecorator = withThemes(ThemeProvider, [theme]); 55 | ``` 56 | 57 | ThemeProvider should accept a theme via `theme` props. This is usually the case for the most common styling libraries like Styled Components, Emotion, Material-UI. 58 | 59 | In case of non standard ThemeProvider you can pass `providerFn` function in options: 60 | 61 | ```js 62 | const providerFn = ({ theme, children }) => { 63 | return {children}; 64 | }; 65 | 66 | const themingDecorator = withThemes(null, [theme], { providerFn }); 67 | ``` 68 | 69 | ## Use your output of the selected value 70 | 71 | ```js 72 | // .storybook/preview.js 73 | 74 | import { ThemeProvider } from 'styled-components'; 75 | import { addDecorator } from '@storybook/react'; 76 | import { withThemes } from '@react-theming/storybook-addon'; 77 | 78 | import { theme } from '../src/theme'; 79 | 80 | ``` 81 | 82 | ### Example getCustomFieldSnippet 83 | 84 | ```js 85 | const selectedValue = { 86 | name: "accent5", 87 | namespace: ["palette", "colors"], 88 | type: "color", 89 | value: "#ac924d" 90 | } 91 | 92 | 93 | const getCustomFieldSnippet = selectedValue => { 94 | const { namespace, name } = selectedValue; 95 | const path = namespace.join('.'); 96 | const fullPath = `${path}.${name}`; 97 | const themeProp = `\${({ theme }) => theme.${fullPath}}`; 98 | return themeProp; 99 | }; 100 | 101 | // The snippet Func function takes the SelectedValue parameter and returns a string 102 | addDecorator(withThemes(ThemeProvider, [theme], { getCustomFieldSnippet })); 103 | 104 | ``` 105 | 106 | ### Example getCustomValueSnippet 107 | 108 | By default, the addon outputs colors in HEX format, if you need some kind of add-in, then pass the colorSnippet parameter. 109 | 110 | ```js 111 | const getCustomValueSnippet = ({value, name, type}) => { 112 | // Here is your code 113 | return value 114 | }; 115 | 116 | // The colorSnipept function accepts an object consisting of { value : HEX, name: string, type: color} 117 | addDecorator(withThemes(ThemeProvider, [theme], { getCustomValueSnippet })); 118 | 119 | ``` 120 | 121 | BACKGROUND COLOR 122 | 123 | This addon has ability to auto change background color when it detect a dark theme. By default it checks if the theme name contains 'dark'. 124 | 125 | You can customize this behavior by passing `onThemeSwitch` function: 126 | 127 | ```js 128 | export const onThemeSwitch = context => { 129 | const { theme } = context; 130 | const background = theme.name === 'Dark theme' ? '#2c2f33' : 'white'; 131 | const parameters = { 132 | backgrounds: { 133 | default: background, 134 | }, 135 | // Pass backgrounds: null to disable background switching at all 136 | }; 137 | return { 138 | parameters, 139 | }; 140 | }; 141 | 142 | const themingDecorator = withThemes(null, [theme], { onThemeSwitch }); 143 | ``` 144 | 145 | This way you can have own checks of what the theme is selected and pass what ever color you need. 146 | 147 | !important: The addon change background color on each theme selecting. In some scenarios you might want to disable this behavior e.g. if you already using addon-backgrounds. You can disable background switching by passing `backgrounds: null` in parameters. 148 | 149 | 150 | Below the use cases for most popular styling libraries: 151 | 152 | ## Using with Emotion 153 | 154 | ```js 155 | // .storybook/preview.js 156 | 157 | import { ThemeProvider } from '@emotion/react'; 158 | import { addDecorator } from '@storybook/react'; 159 | import { withThemes } from '@react-theming/storybook-addon'; 160 | 161 | import { theme } from '../src/theme'; 162 | 163 | // pass ThemeProvider and array of your themes to decorator 164 | addDecorator(withThemes(ThemeProvider, [theme])); 165 | ``` 166 | 167 | 168 | ## 💅 Using with Styled Components 169 | 170 | ```js 171 | // .storybook/preview.js 172 | 173 | import { ThemeProvider } from 'styled-components'; 174 | import { addDecorator } from '@storybook/react'; 175 | import { withThemes } from '@react-theming/storybook-addon'; 176 | 177 | import { theme } from '../src/theme'; 178 | 179 | // pass ThemeProvider and array of your themes to decorator 180 | addDecorator(withThemes(ThemeProvider, [theme])); 181 | ``` 182 | 183 | 184 | ## Using with Material-UI 185 | 186 | ```js 187 | // theme.js 188 | import { red } from '@material-ui/core/colors'; 189 | 190 | // A custom theme for this app 191 | const theme = { 192 | palette: { 193 | primary: { 194 | main: '#556cd6', 195 | }, 196 | secondary: { 197 | main: '#19857b', 198 | }, 199 | error: { 200 | main: red.A400, 201 | }, 202 | background: { 203 | default: '#fff', 204 | }, 205 | }, 206 | }; 207 | 208 | export default theme; 209 | ``` 210 | 211 | ```js 212 | // .storybook/preview.js 213 | 214 | import { ThemeProvider } from '@material-ui/core'; 215 | import { createMuiTheme } from '@material-ui/core/styles'; 216 | import { addDecorator } from '@storybook/react'; 217 | import { withThemes } from '@react-theming/storybook-addon'; 218 | 219 | import theme from '../src/theme'; 220 | 221 | const providerFn = ({ theme, children }) => { 222 | const muTheme = createMuiTheme(theme); 223 | return {children}; 224 | }; 225 | 226 | // pass ThemeProvider and array of your themes to decorator 227 | addDecorator(withThemes(null, [theme], { providerFn })); 228 | ``` 229 | 230 | ```js 231 | // index.js 232 | 233 | import React from 'react'; 234 | import ReactDOM from 'react-dom'; 235 | import { ThemeProvider } from '@material-ui/core/styles'; 236 | import { createMuiTheme } from '@material-ui/core/styles'; 237 | import App from './App'; 238 | import theme from './theme'; 239 | 240 | ReactDOM.render( 241 | 242 | 243 | , 244 | document.querySelector('#root'), 245 | ); 246 | 247 | ``` 248 | 249 | There is an example app with CRA, Material-UI and Storybook Addon [Demo](https://react-theming.github.io/theming-material-ui/) [Source](https://github.com/react-theming/theming-material-ui) 250 | 251 | ## Credits 252 | 253 |
Created with ❤︎ to React and Storybook by Oleg Proskurin [React Theming] 255 |
256 | -------------------------------------------------------------------------------- /docs/common-theme-providers.md: -------------------------------------------------------------------------------- 1 | # UI Libraries with Theme Providers 2 | 3 | ## 1 Emotion 4 | 5 | https://emotion.sh/docs/theming 6 | 7 | ```js 8 | /** @jsx jsx */ 9 | import { jsx } from '@emotion/core'; 10 | import { ThemeProvider } from 'emotion-theming'; 11 | 12 | const theme = { 13 | colors: { 14 | primary: 'hotpink', 15 | }, 16 | }; 17 | 18 | render( 19 | 20 |
({ color: theme.colors.primary })}>some other text
21 |
, 22 | ); 23 | ``` 24 | 25 | ## 2 Styled Components 26 | 27 | https://styled-components.com/docs/advanced#theming 28 | 29 | ```js 30 | import styled, { ThemeProvider } from 'styled-components'; 31 | 32 | // Define our button, but with the use of props.theme this time 33 | const Button = styled.button` 34 | font-size: 1em; 35 | margin: 1em; 36 | padding: 0.25em 1em; 37 | border-radius: 3px; 38 | 39 | /* Color the border and text with theme.main */ 40 | color: ${props => props.theme.main}; 41 | border: 2px solid ${props => props.theme.main}; 42 | `; 43 | 44 | // We are passing a default theme for Buttons that arent wrapped in the ThemeProvider 45 | Button.defaultProps = { 46 | theme: { 47 | main: 'palevioletred', 48 | }, 49 | }; 50 | 51 | // Define what props.theme will look like 52 | const theme = { 53 | main: 'mediumseagreen', 54 | }; 55 | 56 | render( 57 |
58 | 59 | 60 | 61 | 62 | 63 |
, 64 | ); 65 | ``` 66 | 67 | ## 3 Material-UI 68 | 69 | https://material-ui.com/customization/theming/ 70 | 71 | ```js 72 | 73 | 74 | 75 | ``` 76 | -------------------------------------------------------------------------------- /docs/emotion-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-theming/storybook-addon/f3ad642a86fd4e3aa782ee2e663097aa184cdcfb/docs/emotion-logo.png -------------------------------------------------------------------------------- /docs/material-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/storybook-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-theming/storybook-addon/f3ad642a86fd4e3aa782ee2e663097aa184cdcfb/docs/storybook-logo.png -------------------------------------------------------------------------------- /docs/styled-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-theming/storybook-addon/f3ad642a86fd4e3aa782ee2e663097aa184cdcfb/docs/styled-logo.png -------------------------------------------------------------------------------- /docs/theme-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-theming/storybook-addon/f3ad642a86fd4e3aa782ee2e663097aa184cdcfb/docs/theme-panel.png -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "./src", 4 | "./.storybook/register.js", 5 | "../theme-name/dist" 6 | ], 7 | "ext": "js" 8 | 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-theming/storybook-addon", 3 | "description": "Develop themes and themable components with Emotion, Styled Components, Material-UI and your custom solution", 4 | "version": "1.1.10", 5 | "private": false, 6 | "main": "dist/index.js", 7 | "homepage": "https://github.com/react-theming/storybook-addon", 8 | "bugs": { 9 | "url": "https://github.com/react-theming/storybook-addon/issues" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/react-theming/storybook-addon.git" 14 | }, 15 | "author": "Oleg Proskurin (https://github.com/UsulPro)", 16 | "scripts": { 17 | "start-storybook": "start-storybook -p 9001 -s public --ci --no-manager-cache", 18 | "prepare": "package-prepare", 19 | "prestart": "relative-deps", 20 | "predev": "relative-deps", 21 | "start": "nodemon --exec yarn start-storybook", 22 | "dev": "nodemon --exec yarn prepare", 23 | "build-storybook": "build-storybook -s public", 24 | "test": "react-scripts test", 25 | "lint": "eslint src", 26 | "lint:fix": "eslint src --fix", 27 | "deploy-storybook": "storybook-to-ghpages" 28 | }, 29 | "dependencies": { 30 | "@codemirror/theme-one-dark": "0.19.0", 31 | "@focus-reactive/react-yaml": "^1.1.2", 32 | "@react-theming/flatten": "^0.1.1", 33 | "@react-theming/theme-name": "^1.0.3", 34 | "@react-theming/theme-swatch": "^1.0.0", 35 | "@storybook/addon-devkit": "^1.4.2", 36 | "@usulpro/react-json-view": "^2.0.1", 37 | "color-string": "^1.9.1", 38 | "react-color": "^2.18.0" 39 | }, 40 | "relativeDependencies": { 41 | "@react-theming/theme-name": "../theme-name", 42 | "@storybook/addon-devkit": "../addon-development-kit" 43 | }, 44 | "eslintConfig": { 45 | "extends": "react-app" 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | }, 59 | "devDependencies": { 60 | "@babel/cli": "^7.13.10", 61 | "@babel/core": "^7.13.10", 62 | "@babel/plugin-proposal-class-properties": "^7.13.0", 63 | "@babel/preset-env": "^7.13.10", 64 | "@babel/preset-react": "^7.12.13", 65 | "@storybook/addon-actions": "^6.5.9", 66 | "@storybook/addon-backgrounds": "^6.5.9", 67 | "@storybook/addon-links": "^6.5.9", 68 | "@storybook/addons": "^6.5.9", 69 | "@storybook/react": "^6.5.9", 70 | "@storybook/storybook-deployer": "^2.8.7", 71 | "@storybook/theming": "^6.5.9", 72 | "@usulpro/package-prepare": "^1.3.1", 73 | "babel-eslint": "^10.0.2", 74 | "babel-loader": "^8.2.2", 75 | "eslint": "^7.22.0", 76 | "eslint-config-airbnb": "^18.1.0", 77 | "eslint-config-prettier": "^8.1.0", 78 | "eslint-plugin-import": "^2.7.0", 79 | "eslint-plugin-jest": "^24.3.2", 80 | "eslint-plugin-json": "^2.1.1", 81 | "eslint-plugin-jsx-a11y": "^6.0.2", 82 | "eslint-plugin-prettier": "^3.1.2", 83 | "eslint-plugin-react": "^7.1.0", 84 | "nodemon": "^2.0.7", 85 | "prettier": "^2.0.2", 86 | "react": "^17.0.2", 87 | "react-dom": "^17.0.2", 88 | "react-scripts": "3.0.1", 89 | "relative-deps": "^1.0.7", 90 | "storybook-dark-mode": "^1.0.7" 91 | }, 92 | "peerDependencies": { 93 | "@storybook/react": "*", 94 | "@storybook/theming": "*", 95 | "react": "*" 96 | }, 97 | "keywords": [ 98 | "storybook", 99 | "storybook-addons", 100 | "style", 101 | "react", 102 | "material", 103 | "ui", 104 | "material-ui", 105 | "emotion", 106 | "styled-components", 107 | "jss", 108 | "addon", 109 | "decorator", 110 | "theme", 111 | "theming", 112 | "themable", 113 | "editor", 114 | "switch themes", 115 | "change colors", 116 | "tool", 117 | "develop", 118 | "isolation", 119 | "themable components", 120 | "customization", 121 | "dark theme", 122 | "light theme", 123 | "storybook-addon", 124 | "appearance" 125 | ], 126 | "storybook": { 127 | "displayName": "React Theming" 128 | }, 129 | "resolutions": { 130 | "@usulpro/react-json-view": "^2.0.1" 131 | }, 132 | "license": "MIT" 133 | } 134 | -------------------------------------------------------------------------------- /preset.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./dist/preset"); 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-theming/storybook-addon/f3ad642a86fd4e3aa782ee2e663097aa184cdcfb/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /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 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /register.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require("./dist/manager/register"); -------------------------------------------------------------------------------- /src/__test__/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { getTheme, getThemeInfoList } from '../selectors'; 2 | import { createTheme } from '../helpers/sampleTheme'; 3 | 4 | const firstTheme = createTheme({ 5 | mainColor: 'red', 6 | name: 'red-theme', 7 | }); 8 | const secondTheme = createTheme({ 9 | mainColor: 'green', 10 | name: 'green-theme', 11 | }); 12 | const thirdTheme = createTheme({ 13 | mainColor: 'blue', 14 | name: 'blue-theme', 15 | }); 16 | 17 | const store = { 18 | themesList: [firstTheme, secondTheme, thirdTheme], 19 | currentTheme: 1, 20 | }; 21 | 22 | it('should select theme', () => { 23 | expect(getTheme(store)).toEqual(secondTheme); 24 | }); 25 | 26 | it('should select theme-names', () => { 27 | const names = ['red-theme', 'green-theme', 'blue-theme']; 28 | expect(getThemeInfoList(store).map(({ name }) => name)).toEqual(names); 29 | }); 30 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import { getCurrentInd, getSelectedValue, getTheme } from './selectors'; 2 | import { processWord } from './utils'; 3 | 4 | export const setCurrent = (store, ind, api) => { 5 | if (api) { 6 | api.setQueryParams({ 7 | themeInd: ind, 8 | }); 9 | } 10 | return { 11 | ...store, 12 | currentTheme: ind, 13 | selectedWord: null, 14 | }; 15 | }; 16 | 17 | export const selectValue = (store, value) => ({ 18 | ...store, 19 | selectedValue: value, 20 | selectedWord: null, 21 | }); 22 | 23 | export const selectWord = (store, value) => ({ 24 | ...store, 25 | selectedValue: null, 26 | selectedWord: processWord(value), 27 | }); 28 | 29 | export const updateTheme = (store, ind, newTheme) => { 30 | const { themesList } = store; 31 | const newThemesList = [...themesList]; 32 | newThemesList[ind] = newTheme; 33 | const newStore = { 34 | ...store, 35 | themesList: newThemesList, 36 | }; 37 | return newStore; 38 | }; 39 | 40 | const mutateObj = (obj, namespace, key, value) => { 41 | const nestedObj = namespace.reduce((subObj, subKey) => subObj[subKey], obj); 42 | nestedObj[key] = value; 43 | }; 44 | 45 | export const changeSelectedColor = (store, color) => { 46 | const selected = getSelectedValue(store); 47 | if (!selected) return store; 48 | const { name, namespace } = selected; 49 | const theme = getTheme(store); 50 | const ind = getCurrentInd(store); 51 | const themeClone = JSON.parse(JSON.stringify(theme)); 52 | mutateObj(themeClone, namespace, name, color); 53 | return updateTheme(store, ind, themeClone); 54 | }; 55 | 56 | export const changeTheme = (store, newTheme) => { 57 | const ind = getCurrentInd(store); 58 | const { themesList } = store; 59 | const newThemesList = [...themesList]; 60 | newThemesList[ind] = newTheme; 61 | return { 62 | ...store, 63 | themesList: newThemesList, 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import { setConfig } from '@storybook/addon-devkit'; 2 | 3 | setConfig({ 4 | addonId: 'theming', 5 | panelTitle: 'Theming', 6 | }); 7 | -------------------------------------------------------------------------------- /src/helpers/createSelector.js: -------------------------------------------------------------------------------- 1 | export const createSelector = (...args) => { 2 | const resultFn = args.pop(); 3 | return store => { 4 | const selected = args.map(selector => selector(store)); 5 | return resultFn(...selected); 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /src/helpers/sampleTheme.d.ts: -------------------------------------------------------------------------------- 1 | export function createTheme( 2 | theme: T, 3 | ): { 4 | main: string; 5 | textColor: string; 6 | backgroundColor: string; 7 | } & T; 8 | -------------------------------------------------------------------------------- /src/helpers/sampleTheme.js: -------------------------------------------------------------------------------- 1 | const randomColor = () => { 2 | const c = 127 + Math.floor(Math.random() * 128); 3 | return `rgb(${c}, 60, 60)`; 4 | }; 5 | 6 | export const createTheme = theme => ({ 7 | main: randomColor(), 8 | textColor: 'hsl(0, 0%, 30%)', 9 | backgroundColor: 'white', 10 | ...theme, 11 | }); 12 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./preview"; 2 | export * from "./helpers/sampleTheme"; 3 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './preview'; 2 | export * from './helpers/sampleTheme'; 3 | -------------------------------------------------------------------------------- /src/manager/UI/Caption.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as styled from './Caption.styled'; 3 | 4 | const Caption = ({ children }) => {children}; 5 | 6 | export default Caption; 7 | -------------------------------------------------------------------------------- /src/manager/UI/Caption.styled.js: -------------------------------------------------------------------------------- 1 | import { styled } from '@storybook/theming'; 2 | 3 | export const Heading = styled.h3` 4 | font-size: 12px; 5 | margin: 0 4px; 6 | font-weight: 600; 7 | text-transform: capitalize; 8 | `; 9 | -------------------------------------------------------------------------------- /src/manager/UI/IconButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { styled } from '@storybook/theming'; 3 | 4 | const Button = styled.button` 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | padding: 0; 9 | border: none; 10 | border-radius: 2px; 11 | background-color: unset; 12 | height: 20px; 13 | width: 20px; 14 | background-repeat: no-repeat; 15 | background-size: contain; 16 | svg { 17 | fill: ${({ isDark }) => (isDark ? 'white' : 'black')}; 18 | } 19 | 20 | :hover { 21 | background-color: ${({ isDark }) => (isDark ? 'white' : null)}; 22 | svg { 23 | stroke: ${({ isDark }) => (isDark ? '#eeeeee' : '#d4cece')}; 24 | } 25 | } 26 | `; 27 | 28 | const copyIcon = ( 29 | 35 | 36 | 37 | ); 38 | 39 | const icons = { 40 | copy: copyIcon, 41 | }; 42 | 43 | const IconButton = ({ onClick, title, icon, isDark }) => { 44 | const svg = icons[icon]; 45 | return ( 46 | 49 | ); 50 | }; 51 | 52 | export default IconButton; 53 | -------------------------------------------------------------------------------- /src/manager/UI/Text.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { styled } from '@storybook/theming'; 3 | 4 | const Span = styled.span` 5 | margin-left: 10px; 6 | `; 7 | 8 | const Text = ({ children }) => {children}; 9 | 10 | export default Text; 11 | -------------------------------------------------------------------------------- /src/manager/UI/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import * as styled from './Toolbar.styled'; 4 | 5 | const Toolbar = ({ children, footer }) => ( 6 | {children} 7 | ); 8 | 9 | export default Toolbar; 10 | -------------------------------------------------------------------------------- /src/manager/UI/Toolbar.styled.js: -------------------------------------------------------------------------------- 1 | import { styled } from '@storybook/theming'; 2 | 3 | export const Container = styled.div` 4 | background-color: ${({ theme, footer }) => 5 | footer ? theme.background.hoverable : theme.background.hoverable}; 6 | 7 | padding: ${({ footer }) => (footer ? '6px 8px' : '4px 8px')}; 8 | display: flex; 9 | flex-direction: row; 10 | justify-content: flex-start; 11 | align-items: center; 12 | `; 13 | -------------------------------------------------------------------------------- /src/manager/components/ColorDetails.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChromePicker } from 'react-color'; 3 | 4 | import * as styled from './ColorDetails.styled'; 5 | import Toolbar from '../UI/Toolbar'; 6 | import Caption from '../UI/Caption'; 7 | import IconButton from '../UI/IconButton'; 8 | import Text from '../UI/Text'; 9 | import { copyToClipboard } from '../../utils'; 10 | 11 | const ColorDetails = ({ 12 | selectedValue, 13 | selectedWord, 14 | onChange, 15 | isSbDark, 16 | colorSnippet, 17 | }) => { 18 | const { value, name, type } = selectedValue || selectedWord || {}; 19 | const isColor = type === 'color'; 20 | 21 | const handleChange = colorInfo => { 22 | const { hex } = colorInfo; 23 | onChange(hex); 24 | }; 25 | 26 | return ( 27 | 28 | 29 | {name || 'Select color'} 30 | 31 | 32 | {isColor && ( 33 | 34 | )} 35 | 36 | 37 | 43 | {value ? colorSnippet(selectedValue) : 'Select color'} 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default ColorDetails; 50 | -------------------------------------------------------------------------------- /src/manager/components/ColorDetails.styled.js: -------------------------------------------------------------------------------- 1 | import { styled } from '@storybook/theming'; 2 | import { Block } from '@storybook/addon-devkit'; 3 | 4 | export const Container = styled(Block)` 5 | display: flex; 6 | flex-direction: column; 7 | height: auto; 8 | overflow: auto; 9 | label: Container; 10 | `; 11 | 12 | export const PickerHolder = styled.div` 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | background-color: hsla(0, 0%, 50%, 0.35); 17 | height: 1px; 18 | flex-grow: 1; 19 | `; 20 | -------------------------------------------------------------------------------- /src/manager/components/SelectTheme.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { flattenTheme } from '@react-theming/flatten'; 3 | 4 | import * as styled from './SelectTheme.styled'; 5 | import Toolbar from '../UI/Toolbar'; 6 | import Caption from '../UI/Caption'; 7 | import IconButton from '../UI/IconButton'; 8 | 9 | const materialPreview = ({ palette }) => ({ 10 | main: [palette.primary.main, palette.primary.light, palette.primary.dark], 11 | text: [palette.text.secondary], 12 | accent: [ 13 | palette.secondary.main, 14 | palette.secondary.light, 15 | palette.secondary.dark, 16 | ], 17 | background: [palette.text.primary], 18 | }); 19 | 20 | const SelectTheme = ({ themeInfoList, themeInd, setCurrent }) => { 21 | if (!themeInfoList) return 'No themes info'; 22 | const count = themeInfoList.length; 23 | const isMulti = count > 1; 24 | const isSingle = count <= 1; 25 | return ( 26 | 27 | 28 | {isMulti ? `${count} themes` : 'Single Theme'} 29 | 30 | 31 |
    32 | {themeInfoList.map(({ name, theme }, ind) => { 33 | let colorList; 34 | if ( 35 | theme.palette && 36 | theme.palette.primary && 37 | theme.palette.primary.main 38 | ) { 39 | colorList = materialPreview(theme); 40 | } else { 41 | const { flattenColors } = flattenTheme(theme); 42 | colorList = flattenColors.map(({ original }) => original); 43 | } 44 | return ( 45 |
  • 46 | setCurrent(ind)} 48 | current={ind === themeInd} 49 | single={isSingle} 50 | > 51 | 52 | 53 | 54 | 55 | 56 | {name} 57 | 58 |
  • 59 | ); 60 | })} 61 |
62 |
63 | 64 | 65 | 66 |
67 | ); 68 | }; 69 | 70 | export default SelectTheme; 71 | -------------------------------------------------------------------------------- /src/manager/components/SelectTheme.styled.js: -------------------------------------------------------------------------------- 1 | import { styled } from '@storybook/theming'; 2 | import { Block } from '@storybook/addon-devkit'; 3 | import { createSwatch } from '@react-theming/theme-swatch'; 4 | 5 | export const Swatch = createSwatch(styled); 6 | 7 | export const Container = styled(Block)` 8 | display: flex; 9 | flex-direction: column; 10 | height: 100%; 11 | background-color: ${({ theme }) => theme.background.app}; 12 | color: ${({ theme }) => theme.input.color}; 13 | 14 | ul { 15 | list-style: none; 16 | padding: 0; 17 | margin: 0; 18 | } 19 | `; 20 | 21 | export const ListHolder = styled.div` 22 | overflow: auto; 23 | height: 1px; 24 | flex-grow: 1; 25 | padding: ${({ theme }) => theme.layoutMargin}px; 26 | `; 27 | 28 | export const Theme = styled.button` 29 | border: 1px solid ${({ theme }) => theme.input.border}; 30 | ${({ current, theme }) => 31 | current ? `border-color: ${theme.color.secondary} !important;` : null} 32 | 33 | border-radius: ${({ theme }) => theme.appBorderRadius}px; 34 | background-color: ${({ theme }) => theme.background.hoverable}; 35 | margin: ${({ theme }) => Math.floor(theme.layoutMargin / 2)}px 0px; 36 | padding: 0px; 37 | width: 100%; 38 | cursor: pointer; 39 | color: ${({ theme }) => theme.input.color}; 40 | 41 | :hover { 42 | border: 1px solid ${({ theme }) => theme.appBorderColor}; 43 | } 44 | :focus { 45 | outline: 1px solid ${({ theme }) => theme.color.dark}; 46 | outline-offset: 2px; 47 | } 48 | 49 | display: flex; 50 | flex-direction: ${({ single }) => (single ? 'column' : 'row')}; 51 | justify-content: flex-start; 52 | align-items: center; 53 | `; 54 | 55 | export const AvatarHolder = styled.div` 56 | position: relative; 57 | width: ${({ single }) => (single ? '120px' : '36px')}; 58 | height: ${({ single }) => (single ? '120px' : '36px')}; 59 | margin: 16px; 60 | `; 61 | 62 | export const ThemeAvatar = styled.div` 63 | width: 100%; 64 | height: 100%; 65 | `; 66 | 67 | export const Title = styled.h4` 68 | margin-left: 6px; 69 | font-size: ${({ single }) => (single ? '32px' : '16px')}; 70 | font-weight: ${({ single }) => (single ? 'bold' : 'normal')}; 71 | `; 72 | -------------------------------------------------------------------------------- /src/manager/components/ThemeBrowser.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import * as styled from './ThemeBrowser.styled'; 4 | import Toolbar from '../UI/Toolbar'; 5 | import Caption from '../UI/Caption'; 6 | import IconButton from '../UI/IconButton'; 7 | import Text from '../UI/Text'; 8 | import { copyToClipboard } from '../../utils/clipboard'; 9 | import { initialEditors, useEditors } from '../editors/useEditors'; 10 | import { defaultSnippet } from '../../utils/default'; 11 | 12 | const showThemePath = (selectedValue, fieldSnippetFn) => { 13 | if (!selectedValue) return 'Select value'; 14 | try { 15 | const fn = fieldSnippetFn || defaultSnippet; 16 | return fn(selectedValue); 17 | } catch (err) { 18 | return 'try to select value'; 19 | } 20 | }; 21 | 22 | const ThemeBrowser = ({ 23 | theme, 24 | isSbDark, 25 | selectValue, 26 | selectWord, 27 | selectedValue, 28 | updateTheme, 29 | fieldSnippetFn, 30 | }) => { 31 | const editors = useEditors(initialEditors); 32 | 33 | const footerAction = showThemePath(selectedValue, fieldSnippetFn); 34 | 35 | const handlerChange = value => updateTheme(value.json); 36 | 37 | return ( 38 | 39 | 40 | Editor: 41 | 42 | {editors.editorButtons.map(btn => ( 43 | 51 | ))} 52 | 53 | 54 | 55 | {editors.renderCurrentEditor({ 56 | isDark: isSbDark, 57 | theme, 58 | onChange: handlerChange, 59 | selectValue, 60 | selectWord, 61 | })} 62 | 63 | {footerAction ? ( 64 | 65 | 71 | {footerAction} 72 | 73 | ) : null} 74 | 75 | ); 76 | }; 77 | 78 | export default ThemeBrowser; 79 | -------------------------------------------------------------------------------- /src/manager/components/ThemeBrowser.styled.js: -------------------------------------------------------------------------------- 1 | import { styled } from '@storybook/theming'; 2 | import { Block } from '@storybook/addon-devkit'; 3 | 4 | export const Container = styled(Block)` 5 | border-left: 1px solid gray; 6 | border-right: 1px solid gray; 7 | display: flex; 8 | flex-direction: column; 9 | height: auto; 10 | label: Container; 11 | `; 12 | 13 | export const ThemeHolder = styled.div` 14 | height: auto; 15 | overflow: auto; 16 | flex-grow: 1; 17 | label: ThemeHolder; 18 | `; 19 | 20 | const copyIcon = 21 | 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTE2IDFINGMtMS4xIDAtMiAuOS0yIDJ2MTRoMlYzaDEyVjF6bTMgNEg4Yy0xLjEgMC0yIC45LTIgMnYxNGMwIDEuMS45IDIgMiAyaDExYzEuMSAwIDItLjkgMi0yVjdjMC0xLjEtLjktMi0yLTJ6bTAgMTZIOFY3aDExdjE0eiIvPjwvc3ZnPg='; 22 | 23 | export const Copy = styled.button` 24 | background-color: unset; 25 | border: none; 26 | background: url(${copyIcon}); 27 | background-repeat: no-repeat; 28 | background-size: contain; 29 | width: 35px; 30 | height: 20px; 31 | cursor: pointer; 32 | opacity: 0.6; 33 | :hover { 34 | opacity: 1; 35 | } 36 | `; 37 | 38 | export const SelectedCard = styled.div` 39 | background-color: #f6f9fc; 40 | padding: 12px; 41 | margin-top: 4px; 42 | font-size: 16px; 43 | `; 44 | 45 | export const ButtonsEditor = styled.div` 46 | display: flex; 47 | button { 48 | position: relative; 49 | background-color: ${({ isDark }) => (isDark ? '#525252' : '#cbcbcb')}; 50 | border: none; 51 | border-radius: 2px; 52 | font-size: 11px; 53 | font-weight: 800; 54 | padding: 1px 8px; 55 | margin: 0 0 0 10px; 56 | color: ${({ isDark }) => (isDark ? '#bcbaba' : '#565454')}; 57 | cursor: pointer; 58 | 59 | :hover { 60 | opacity: 0.7; 61 | } 62 | } 63 | button.active { 64 | :hover { 65 | opacity: 1; 66 | } 67 | ::after { 68 | content: '🟢'; 69 | font-size: 4px; 70 | position: absolute; 71 | top: 1px; 72 | left: 1px; 73 | } 74 | } 75 | `; 76 | -------------------------------------------------------------------------------- /src/manager/editors/ReactJsonEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactJson from '@usulpro/react-json-view'; 3 | 4 | const ReactJsonEditor = ({ isDark, theme, selectValue }) => { 5 | const jsTheme = isDark ? 'codeschool' : 'shapeshifter:inverted'; 6 | return ( 7 | 8 | ); 9 | }; 10 | 11 | export default ReactJsonEditor; 12 | -------------------------------------------------------------------------------- /src/manager/editors/ReactYamlEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import YamlEditor from '@focus-reactive/react-yaml'; 3 | import { oneDark } from '@codemirror/theme-one-dark'; 4 | 5 | const ReactYamlEditor = ({ isDark, theme, onChange, selectWord }) => { 6 | const merge = React.useCallback(({ json }) => ({ json }), []); 7 | const ownTheme = isDark ? oneDark : undefined; 8 | 9 | const handleSelectWord = ({ word }) => { 10 | selectWord(word); 11 | }; 12 | 13 | return ( 14 | 21 | ); 22 | }; 23 | 24 | export default ReactYamlEditor; 25 | -------------------------------------------------------------------------------- /src/manager/editors/useEditors.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactJsonEditor from './ReactJsonEditor'; 3 | import ReactYamlEditor from './ReactYamlEditor'; 4 | 5 | export const initialEditors = [ 6 | { 7 | name: 'json', 8 | title: 'JSON', 9 | renderComponent: props => , 10 | }, 11 | { 12 | name: 'yaml', 13 | title: 'YAML', 14 | renderComponent: props => , 15 | }, 16 | ]; 17 | 18 | export const useEditors = registeredEditors => { 19 | const editorNames = [...new Set(registeredEditors.map(({ name }) => name))]; 20 | const [currentEditor, setCurrentEditor] = React.useState(editorNames[0]); 21 | 22 | const editorButtons = registeredEditors.map(editor => ({ 23 | ...editor, 24 | isSelected: editor.name === currentEditor, 25 | select: () => setCurrentEditor(editor.name), 26 | })); 27 | 28 | const renderCurrentEditor = props => { 29 | const editor = registeredEditors.find(({ name }) => name === currentEditor); 30 | return editor.renderComponent(props); 31 | }; 32 | 33 | return { 34 | editorButtons, 35 | renderCurrentEditor, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/manager/register.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { register, Layout } from '@storybook/addon-devkit'; 3 | import { useTheme } from '@storybook/theming'; 4 | 5 | import { 6 | getTheme, 7 | getThemeInfoList, 8 | getThemeInfo, 9 | getSelectedValue, 10 | getCurrentInd, 11 | getSnippet, 12 | getSelectedWord, 13 | getColorSnippet, 14 | } from '../selectors'; 15 | import SelectTheme from './components/SelectTheme'; 16 | import ThemeBrowser from './components/ThemeBrowser'; 17 | 18 | import '../config'; 19 | import ColorDetails from './components/ColorDetails'; 20 | import * as actions from '../actions'; 21 | 22 | const AddonThemingPanel = ({ 23 | theme, 24 | themeInd, 25 | themeInfoList, 26 | themeInfo, 27 | selectedValue, 28 | selectedWord, 29 | setCurrent, 30 | selectValue, 31 | selectWord, 32 | changeSelectedColor, 33 | isFirstDataReceived, 34 | api, 35 | snippet, 36 | colorSnippet, 37 | updateTheme, 38 | }) => { 39 | React.useEffect(() => { 40 | if (themeInd === null) { 41 | const storedThemeInd = api.getQueryParam('themeInd'); 42 | setCurrent(storedThemeInd || 0); 43 | } 44 | }, [themeInd]); 45 | 46 | const sbTheme = useTheme(); 47 | const isSbDark = sbTheme.base !== 'light'; 48 | 49 | return isFirstDataReceived && themeInd !== null ? ( 50 | 51 | 56 | 66 | 73 | 74 | ) : ( 75 |

Waiting for data

76 | ); 77 | }; 78 | 79 | register( 80 | { 81 | themeInfoList: getThemeInfoList, 82 | theme: getTheme, 83 | themeInfo: getThemeInfo, 84 | themeInd: getCurrentInd, 85 | selectedValue: getSelectedValue, 86 | selectedWord: getSelectedWord, 87 | snippet: getSnippet, 88 | colorSnippet: getColorSnippet, 89 | }, 90 | ({ global }) => ({ 91 | setCurrent: global(actions.setCurrent), 92 | selectValue: global(actions.selectValue), 93 | selectWord: global(actions.selectWord), 94 | changeSelectedColor: global(actions.changeSelectedColor), 95 | updateTheme: global(actions.changeTheme), 96 | }), 97 | )(AddonThemingPanel); 98 | -------------------------------------------------------------------------------- /src/preset/index.js: -------------------------------------------------------------------------------- 1 | export function config(entry = []) { 2 | // return [...entry, require.resolve("./preview")] 3 | return [...entry]; 4 | } 5 | 6 | export function managerEntries(entry = []) { 7 | return [...entry, require.resolve('../manager/register')]; 8 | } 9 | -------------------------------------------------------------------------------- /src/preset/preview.js: -------------------------------------------------------------------------------- 1 | import { withThemes } from '../preview'; 2 | 3 | export const decorators = [withThemes]; 4 | -------------------------------------------------------------------------------- /src/preview/index.d.ts: -------------------------------------------------------------------------------- 1 | import { StoryFn } from '@storybook/addon-devkit'; 2 | import React from 'react'; 3 | 4 | type ThemeProviderProps = { 5 | theme: T; 6 | children: React.ReactNode; 7 | } & P; 8 | 9 | type Opts = { 10 | providerFn: React.FC>; 11 | onThemeSwitch?: (context: any) => {}; 12 | }; 13 | 14 | export function withThemes( 15 | ThemeProvider: React.ComponentType>, 16 | themesList: {}[], 17 | opts: Opts, 18 | ): ReturnType; 19 | 20 | export const toThemes: < 21 | T extends import('@storybook/addon-devkit').AddonParameters 22 | >( 23 | _: T, 24 | ) => { [key: symbol]: T }; 25 | 26 | export function useThemes( 27 | ThemeProvider: React.ComponentType>, 28 | themesList: {}[], 29 | opts: Opts, 30 | ): typeof toThemes; 31 | 32 | -------------------------------------------------------------------------------- /src/preview/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { addDecorator } from '@storybook/react'; 3 | import { createDecorator, setParameters } from '@storybook/addon-devkit'; 4 | import '../config'; 5 | import { handleOnSwitch, onThemeSwitchDefault } from './onThemeSwitch'; 6 | import { defaultColorSnippet, defaultSnippet } from '../utils/default'; 7 | 8 | const DecoratorUI = ThemeProvider => ({ 9 | getStory, 10 | theme, 11 | themeInd, 12 | onThemeSwitch, 13 | }) => { 14 | React.useEffect(() => { 15 | handleOnSwitch({ theme, onThemeSwitch }); 16 | }, [themeInd]); 17 | return {getStory()}; 18 | }; 19 | 20 | const withData = (ThemeProvider, { providerFn, onThemeSwitch }) => { 21 | let CurrentThemeProvider = ThemeProvider; 22 | if (providerFn) { 23 | CurrentThemeProvider = ({ theme, children }) => ( 24 | <>{providerFn({ theme, children })} 25 | ); 26 | } 27 | return createDecorator({ 28 | theme: store => store.themesList[store.currentTheme || 0], 29 | themeInd: store => store.currentTheme, 30 | onThemeSwitch: () => onThemeSwitch, 31 | })(DecoratorUI(CurrentThemeProvider), { isGlobal: true }); 32 | }; 33 | 34 | export const withThemes = ( 35 | ThemeProvider, 36 | themesList, 37 | { 38 | providerFn, 39 | onThemeSwitch = onThemeSwitchDefault, 40 | getCustomFieldSnippet = defaultSnippet, 41 | getCustomValueSnippet = defaultColorSnippet, 42 | } = {}, 43 | ) => 44 | withData(ThemeProvider, { providerFn, onThemeSwitch })({ 45 | themesList, 46 | currentTheme: null, 47 | fieldSnippetFn: getCustomFieldSnippet, 48 | colorSnippet: getCustomValueSnippet, 49 | }); 50 | 51 | export const toThemes = setParameters(); 52 | 53 | export const useThemes = (...args) => { 54 | addDecorator(withThemes(...args)); 55 | return toThemes; 56 | }; 57 | -------------------------------------------------------------------------------- /src/preview/onThemeSwitch.js: -------------------------------------------------------------------------------- 1 | const LIGHT_BG = '#fff'; 2 | const DARK_BG = '#333'; 3 | 4 | const TAG_SELECTOR = 'addon-backgrounds-color'; 5 | 6 | const createStyle = color => ` 7 | .sb-show-main { 8 | background: ${color} !important; 9 | transition: background-color 0.3s; 10 | } 11 | `; 12 | 13 | export const addBackgroundStyle = color => { 14 | if (!color) { 15 | return; 16 | } 17 | const css = createStyle(color); 18 | const existingStyle = document.getElementById(TAG_SELECTOR); 19 | if (existingStyle) { 20 | if (existingStyle.innerHTML !== css) { 21 | existingStyle.innerHTML = css; 22 | } 23 | } else { 24 | const style = document.createElement('style'); 25 | style.setAttribute('id', TAG_SELECTOR); 26 | style.innerHTML = css; 27 | 28 | document.head.appendChild(style); 29 | } 30 | }; 31 | 32 | export const handleOnSwitch = ({ theme, onThemeSwitch }) => { 33 | const result = onThemeSwitch({ theme }); 34 | const color = result.parameters?.backgrounds?.default; 35 | addBackgroundStyle(color); 36 | }; 37 | 38 | export const onThemeSwitchDefault = context => { 39 | const { theme } = context; 40 | const background = /dark/i.test(theme.name) ? DARK_BG : LIGHT_BG; 41 | const parameters = { 42 | backgrounds: { 43 | default: background, 44 | }, 45 | }; 46 | return { 47 | parameters, 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /src/register.js: -------------------------------------------------------------------------------- 1 | import './manager/register'; 2 | -------------------------------------------------------------------------------- /src/selectors.js: -------------------------------------------------------------------------------- 1 | import { themeName } from '@react-theming/theme-name'; 2 | 3 | export const createSelector = (...args) => { 4 | const resultFn = args.pop(); 5 | return store => { 6 | const selected = args.map(selector => selector(store)); 7 | return resultFn(...selected, store); 8 | }; 9 | }; 10 | 11 | export const getCurrentInd = store => store.currentTheme; 12 | export const getThemesList = store => store.themesList; 13 | export const getSnippet = store => store.fieldSnippetFn; 14 | export const getColorSnippet = store => store.colorSnippet; 15 | 16 | export const getTheme = createSelector( 17 | getCurrentInd, 18 | getThemesList, 19 | (ind, themes) => (themes ? themes[ind] : undefined), 20 | ); 21 | 22 | export const getThemeInfoList = createSelector(getThemesList, (list = []) => 23 | list.map((theme, ind) => ({ 24 | name: themeName(theme, ind), 25 | theme, 26 | })), 27 | ); 28 | 29 | export const getThemeInfo = createSelector( 30 | getCurrentInd, 31 | getThemeInfoList, 32 | (ind, themesInfo) => (themesInfo ? themesInfo[ind] : undefined), 33 | ); 34 | 35 | export const getSelectedValue = createSelector(getTheme, (theme, store) => { 36 | const { selectedValue } = store; 37 | if (!selectedValue) return undefined; 38 | const { name, namespace, type } = selectedValue; 39 | const nestedObj = namespace.reduce((subObj, subKey) => subObj[subKey], theme); 40 | const value = nestedObj[name]; 41 | return { name, namespace, value, type }; 42 | }); 43 | 44 | export const getSelectedWord = createSelector(store => { 45 | const { selectedWord } = store; 46 | return selectedWord; 47 | }); 48 | -------------------------------------------------------------------------------- /src/utils/clipboard.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | export const copyToClipboard = str => () => { 3 | const el = window.document.createElement('textarea'); 4 | el.value = str; 5 | window.document.body.appendChild(el); 6 | el.select(); 7 | window.document.execCommand('copy'); 8 | window.document.body.removeChild(el); 9 | }; 10 | -------------------------------------------------------------------------------- /src/utils/colors.js: -------------------------------------------------------------------------------- 1 | import colorString from 'color-string'; 2 | 3 | export const isColor = str => colorString.get(str); 4 | 5 | export const processWord = str => { 6 | if (!str) { 7 | return null; 8 | } 9 | const value = str.replace(/^['|"|`|(]|['|"|`|)|:]*$/g, ''); 10 | if (!value) { 11 | return null; 12 | } 13 | return { 14 | name: '/* yaml */', 15 | type: isColor(value) ? 'color' : '', 16 | value, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/utils/default.js: -------------------------------------------------------------------------------- 1 | export const defaultSnippet = selectedValue => { 2 | const { namespace, name } = selectedValue; 3 | const path = namespace.join('.'); 4 | const fullPath = `${path}.${name}`; 5 | const themeProp = `\${({ theme }) => theme.${fullPath}}`; 6 | return themeProp; 7 | }; 8 | 9 | export const defaultColorSnippet = selectedValue => selectedValue?.value || ''; 10 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from './clipboard'; 2 | export * from './default'; 3 | export * from './colors'; 4 | --------------------------------------------------------------------------------