├── .babelrc
├── .cspell.json
├── .editorconfig
├── .env
├── .eslintignore
├── .eslintrc.json
├── .github
├── CONTRIBUTING.md
├── FUNDING.yml
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── main.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .storybook
├── main.js
└── preview.js
├── .vscode
├── extensions.json
├── launch.json
└── settings.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── README.md
├── config
├── tsconfig.build.json
└── tsconfig.module.build.json
├── cypress.config.ts
├── cypress.d.ts
├── cypress
├── fixtures
│ └── example.json
├── plugins
│ └── index.js
├── support
│ ├── commands.ts
│ ├── component-index.html
│ └── component.tsx
└── videos
│ ├── Badge
│ └── Badge.cy.tsx.mp4
│ ├── Button
│ └── Button.cy.tsx.mp4
│ ├── Checkbox
│ └── Checkbox.cy.tsx.mp4
│ ├── Container
│ └── Container.cy.tsx.mp4
│ ├── Grid
│ └── Grid.cy.tsx.mp4
│ ├── Input
│ └── Input.cy.tsx.mp4
│ ├── Modal
│ └── Modal.cy.tsx.mp4
│ ├── Popover
│ └── Popover.cy.tsx.mp4
│ ├── Radio
│ └── Radio.cy.tsx.mp4
│ ├── Switch
│ └── Switch.cy.tsx.mp4
│ └── Text
│ └── Text.cy.tsx.mp4
├── package-lock.json
├── package.json
├── public
└── cover.png
├── setupTests.ts
├── src
├── index.ts
└── lib
│ ├── Badge
│ ├── Badge.cy.tsx
│ ├── Badge.stories.tsx
│ ├── Badge.styles.tsx
│ ├── Badge.test.tsx
│ ├── Badge.tsx
│ ├── __snapshots__
│ │ └── Badge.test.tsx.snap
│ └── index.ts
│ ├── Box
│ ├── Box.stories.tsx
│ ├── Box.styles.tsx
│ ├── Box.test.tsx
│ ├── Box.tsx
│ ├── __snapshots__
│ │ └── Box.test.tsx.snap
│ └── index.ts
│ ├── Button
│ ├── Button.cy.tsx
│ ├── Button.stories.tsx
│ ├── Button.styles.ts
│ ├── Button.test.tsx
│ ├── Button.tsx
│ ├── ButtonIcon.tsx
│ ├── __snapshots__
│ │ └── Button.test.tsx.snap
│ └── index.ts
│ ├── Checkbox
│ ├── Checkbox.cy.tsx
│ ├── Checkbox.stories.tsx
│ ├── Checkbox.styles.tsx
│ ├── Checkbox.test.tsx
│ ├── Checkbox.tsx
│ ├── CheckboxGroup
│ │ ├── CheckboxGroup.stories.tsx
│ │ ├── CheckboxGroup.styles.tsx
│ │ ├── CheckboxGroup.test.tsx
│ │ ├── CheckboxGroup.tsx
│ │ ├── __snapshots__
│ │ │ └── CheckboxGroup.test.tsx.snap
│ │ └── index.ts
│ ├── __snapshots__
│ │ └── Checkbox.test.tsx.snap
│ └── index.ts
│ ├── Container
│ ├── Container.cy.tsx
│ ├── Container.stories.tsx
│ ├── Container.styles.ts
│ ├── Container.test.tsx
│ ├── Container.tsx
│ ├── __snapshots__
│ │ └── Container.test.tsx.snap
│ └── index.ts
│ ├── Grid
│ ├── Grid.cy.tsx
│ ├── Grid.stories.tsx
│ ├── Grid.styles.tsx
│ ├── Grid.test.tsx
│ ├── Grid.tsx
│ ├── GridContainer.tsx
│ ├── GridRuler.tsx
│ ├── __snapshots__
│ │ └── Grid.test.tsx.snap
│ └── index.ts
│ ├── Input
│ ├── Input.cy.tsx
│ ├── Input.stories.tsx
│ ├── Input.styles.ts
│ ├── Input.test.tsx
│ ├── Input.tsx
│ ├── __snapshots__
│ │ └── Input.test.tsx.snap
│ └── index.ts
│ ├── Modal
│ ├── Modal.cy.tsx
│ ├── Modal.stories.tsx
│ ├── Modal.styles.tsx
│ ├── Modal.test.tsx
│ ├── Modal.tsx
│ ├── ModalBody.tsx
│ ├── ModalFooter.tsx
│ ├── ModalHeader.tsx
│ ├── __snapshots__
│ │ └── Modal.test.tsx.snap
│ └── index.ts
│ ├── Popover
│ ├── Popover.cy.tsx
│ ├── Popover.stories.tsx
│ ├── Popover.styles.tsx
│ ├── Popover.test.tsx
│ ├── Popover.tsx
│ ├── PopoverContent.tsx
│ ├── PopoverTrigger.tsx
│ ├── __snapshots__
│ │ └── Popover.test.tsx.snap
│ └── index.ts
│ ├── Radio
│ ├── Radio.cy.tsx
│ ├── Radio.stories.tsx
│ ├── Radio.styles.tsx
│ ├── Radio.test.tsx
│ ├── Radio.tsx
│ ├── RadioGroup
│ │ ├── RadioGroup.stories.tsx
│ │ ├── RadioGroup.styles.tsx
│ │ ├── RadioGroup.test.tsx
│ │ ├── RadioGroup.tsx
│ │ ├── __snapshots__
│ │ │ └── RadioGroup.test.tsx.snap
│ │ └── index.ts
│ ├── __snapshots__
│ │ └── Radio.test.tsx.snap
│ └── index.ts
│ ├── Switch
│ ├── Switch.cy.tsx
│ ├── Switch.stories.tsx
│ ├── Switch.styles.tsx
│ ├── Switch.test.tsx
│ ├── Switch.tsx
│ ├── __snapshots__
│ │ └── Switch.test.tsx.snap
│ └── index.ts
│ ├── Text
│ ├── Text.cy.tsx
│ ├── Text.stories.tsx
│ ├── Text.styles.tsx
│ ├── Text.test.tsx
│ ├── Text.tsx
│ ├── __snapshots__
│ │ └── Text.test.tsx.snap
│ └── index.ts
│ ├── Theme
│ ├── DecaUIProvider.test.tsx
│ ├── DecaUIProvider.tsx
│ ├── __snapshots__
│ │ └── DecaUIProvider.test.tsx.snap
│ ├── index.ts
│ └── stitches.config.ts
│ └── Utils
│ ├── color.ts
│ ├── env.ts
│ ├── hooks.ts
│ ├── index.ts
│ ├── random.ts
│ ├── refs.ts
│ ├── test.ts
│ ├── types.ts
│ └── utils.test.tsx
├── tsconfig.json
├── tsconfig.module.json
└── vite.config.ts
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-flow"]
3 | }
4 |
--------------------------------------------------------------------------------
/.cspell.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2",
3 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json",
4 | "language": "en",
5 | "words": [
6 | "bitjson",
7 | "bitauth",
8 | "cimg",
9 | "circleci",
10 | "codecov",
11 | "commitlint",
12 | "dependabot",
13 | "editorconfig",
14 | "esnext",
15 | "execa",
16 | "exponentiate",
17 | "globby",
18 | "libauth",
19 | "mkdir",
20 | "prettierignore",
21 | "sandboxed",
22 | "transpiled",
23 | "typedoc",
24 | "untracked"
25 | ],
26 | "flagWords": [],
27 | "ignorePaths": [
28 | "package.json",
29 | "package-lock.json",
30 | "yarn.lock",
31 | "tsconfig.json",
32 | "node_modules/**"
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | indent_style = space
9 | insert_final_newline = true
10 | max_line_length = 80
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | max_line_length = 0
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | NODE_ENV="development"
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | package.json
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": { "project": ["./tsconfig.json", "./setupTests.ts"], "warnOnUnsupportedTypeScriptVersion": false
5 | },
6 | "env": { "es6": true },
7 | "ignorePatterns": ["node_modules", "build", "coverage", "setupTests.ts"],
8 | "plugins": ["import", "eslint-comments", "jest-dom", "testing-library"],
9 | "extends": [
10 | "eslint:recommended",
11 | "plugin:cypress/recommended",
12 | "plugin:eslint-comments/recommended",
13 | "plugin:@typescript-eslint/recommended",
14 | "plugin:import/typescript",
15 | "plugin:storybook/recommended",
16 | "prettier",
17 | "prettier/@typescript-eslint"
18 | ],
19 | "globals": { "BigInt": true, "console": true, "WebAssembly": true },
20 | "rules": {
21 | "@typescript-eslint/no-explicit-any": "off",
22 | "@typescript-eslint/explicit-module-boundary-types": "off",
23 | "no-unused-vars": "off",
24 | "@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }],
25 | "eslint-comments/disable-enable-pair": [
26 | "error",
27 | { "allowWholeFile": true }
28 | ],
29 | "eslint-comments/no-unused-disable": "error",
30 | "import/order":"off",
31 | "sort-imports":"off"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Example Contributing Guidelines
2 |
3 | This is an example of GitHub's contributing guidelines file. Check out GitHub's [CONTRIBUTING.md help center article](https://help.github.com/articles/setting-guidelines-for-repository-contributors/) for more information.
4 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: deca-ui
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | - **I'm submitting a ...**
2 | [ ] bug report
3 | [ ] feature request
4 | [ ] question about the decisions made in the repository
5 | [ ] question about how to use this project
6 |
7 | - **Summary**
8 |
9 | - **Other information** (e.g. detailed explanation, stack traces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.)
10 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | - **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...)
2 |
3 | - **What is the current behavior?** (You can also link to an open issue here)
4 |
5 | - **What is the new behavior (if this is a feature change)?**
6 |
7 | - **Other information**:
8 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI/CD
2 | on: [push]
3 | jobs:
4 | quality:
5 | runs-on: ubuntu-latest
6 | strategy:
7 | matrix:
8 | node-version: [14.x, 16.x]
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v3
12 | - name: Use Node.js ${{ matrix.node-version }}
13 | uses: actions/setup-node@v3
14 | with:
15 | node-version: ${{ matrix.node-version }}
16 | cache: 'npm'
17 |
18 | - name: Install dependencies
19 | run: npm ci
20 | - name: Test
21 | run: npm test
22 | - name: Codecov
23 | uses: codecov/codecov-action@v3.1.0
24 | publish:
25 | runs-on: ubuntu-latest
26 | if: ${{ github.ref == 'refs/heads/main' }}
27 | needs: [quality]
28 | steps:
29 | - name: Checkout
30 | uses: actions/checkout@v3
31 | with:
32 | fetch-depth: 0
33 | - name: Setup Node.js
34 | uses: actions/setup-node@v3
35 | with:
36 | node-version: 16.x
37 | cache: 'npm'
38 | - name: Install Packages
39 | run: npm ci
40 | - name: Build
41 | run: npm run build
42 | - name: Generate Release Body
43 | run: npm run extract-changelog > RELEASE_BODY.md
44 | - name: Publish to NPM
45 | uses: JS-DevTools/npm-publish@v1
46 | with:
47 | access: "public"
48 | token: ${{ secrets.NPM_TOKEN }}
49 | - name: 'Get Latest Tag'
50 | id: latestTag
51 | uses: "WyriHaximus/github-action-get-previous-tag@v1"
52 | - name: 'Get Latest Release'
53 | id: latestRelease
54 | uses: pozetroninc/github-action-get-latest-release@master
55 | with:
56 | repository: ${{ github.repository }}
57 | - name: Create GitHub Release
58 | if: ${{ steps.latestTag.outputs.tag != steps.latestRelease.outputs.release }}
59 | uses: ncipollo/release-action@v1
60 | with:
61 | bodyFile: "RELEASE_BODY.md"
62 | token: ${{ secrets.GITHUB_TOKEN }}
63 | tag: ${{ steps.latestTag.outputs.tag }}
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/*
2 | .nyc_output
3 | build
4 | node_modules
5 | test
6 | src/**.js
7 | coverage
8 | *.log
9 | yarn.lock
10 | storybook-static
11 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | legacy-peer-deps=true
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # package.json is formatted by package managers, so we ignore it here
2 | package.json
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
2 |
3 | module.exports = {
4 | core: {
5 | builder: '@storybook/builder-webpack5',
6 | },
7 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
8 | addons: [
9 | '@storybook/addon-links',
10 | '@storybook/addon-essentials',
11 | '@storybook/addon-interactions',
12 | '@storybook/addon-docs',
13 | ],
14 | typescript: {
15 | check: false,
16 | checkOptions: {},
17 | reactDocgen: 'react-docgen-typescript',
18 | },
19 | framework: '@storybook/react',
20 | webpackFinal: async (config, { configType }) => {
21 | config.resolve.plugins = [new TsconfigPathsPlugin()];
22 | return config;
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import { DecaUIProvider } from '@lib/Theme';
2 | const { addDecorator } = require('@storybook/react');
3 | const { withPropsTable } = require('storybook-addon-react-docgen');
4 |
5 | export const parameters = {
6 | backgrounds: {
7 | values: [
8 | {
9 | name: 'dark',
10 | value: '#000000',
11 | },
12 | ],
13 | },
14 | actions: { argTypesRegex: '^on[A-Z].*' },
15 | controls: {
16 | matchers: {
17 | color: /(background|color)$/i,
18 | date: /Date$/,
19 | },
20 | },
21 | };
22 |
23 | export const decorators = [
24 | (Story) => (
25 | // adds provider for stories that do not contain it
26 | // (only to see changes made to component by css reset)
27 |
28 |
29 |
30 | ),
31 | ];
32 |
33 | addDecorator(withPropsTable);
34 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "esbenp.prettier-vscode",
5 | "eamodio.gitlens",
6 | "streetsidesoftware.code-spell-checker",
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | // To debug, make sure a *.spec.ts file is active in the editor, then run a configuration
5 | {
6 | "type": "node",
7 | "request": "launch",
8 | "name": "Debug Active Spec",
9 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava",
10 | "runtimeArgs": ["debug", "--break", "--serial", "${file}"],
11 | "port": 9229,
12 | "outputCapture": "std",
13 | "skipFiles": ["/**/*.js"],
14 | "preLaunchTask": "npm: build"
15 | // "smartStep": true
16 | },
17 | {
18 | // Use this one if you're already running `yarn watch`
19 | "type": "node",
20 | "request": "launch",
21 | "name": "Debug Active Spec (no build)",
22 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava",
23 | "runtimeArgs": ["debug", "--break", "--serial", "${file}"],
24 | "port": 9229,
25 | "outputCapture": "std",
26 | "skipFiles": ["/**/*.js"]
27 | // "smartStep": true
28 | }]
29 | }
30 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.userWords": [], // only use words from .cspell.json
3 | "cSpell.enabled": true,
4 | "editor.formatOnSave": true,
5 | "typescript.tsdk": "node_modules/typescript/lib",
6 | "typescript.enablePromptUseWorkspaceTsdk": true
7 | }
8 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # DecaUI Contributing Guide
2 |
3 | Hello!, I am very excited that you are interested in contributing with DecaUI. However, before submitting your contribution, be sure to take a moment and read the following guidelines.
4 |
5 | - [Code of Conduct](https://github.com/deca-org/deca-ui/blob/main/CODE_OF_CONDUCT.md)
6 | - [Extraction request guidelines](#pull-request-guidelines)
7 | - [Development Setup](#development-setup)
8 | - [Visual Changes](#visual-changes)
9 | - [Documentation](#documentation)
10 |
11 | ## Pull Request Guidelines
12 |
13 | - The `main` branch is basically a snapshot of the latest stable version. All development must be done in dedicated branches.
14 | - Make sure that Github Actions are green
15 | - It is good to have multiple small commits while working on the PR. We'll let GitHub squash it automatically before the merge.
16 | - If you add a new feature:
17 | - Add the test case that accompanies it.
18 | - Provide a compelling reason to add this feature. Ideally, I would first open a suggestion topic and green it before working on it.
19 | - If you correct an error:
20 | - If you are solving a special problem, add (fix #xxxx [, # xxx]) (# xxxx is the problem identification) in your PR title for a better launch record, for example update entities encoding / decoding (fix # 3899).
21 | - Provide a detailed description of the error in the PR. Favorite live demo.
22 | - Add the appropriate test coverage, if applicable.
23 |
24 | ## Development Setup
25 |
26 | After cloning the repository, execute the following commands in the root folder:
27 |
28 | 1. Install dependencies
29 |
30 | ```bash
31 | npm install
32 | ```
33 |
34 | 2. Edit component / docs code
35 |
36 | 3. Previewing components using storybook
37 |
38 | You can use this command to start up storybook:
39 |
40 | ```bash
41 | npm run storybook
42 | ```
43 |
44 | 4. Test your code
45 |
46 | ```bash
47 | npm run test
48 | ```
49 |
50 | 5. Create a branch for your feature or fix:
51 |
52 | ```bash
53 | # Move into a new branch for your feature
54 | git checkout -b feat/thing
55 | ```
56 |
57 | ```bash
58 | # Move into a new branch for your fix
59 | git checkout -b fix/something
60 | ```
61 |
62 | 6. If your code passes all the tests, then push your feature/fix branch:
63 |
64 | All commits that fix bugs or add features need a test.
65 |
66 | 7. Be sure the package builds.
67 |
68 | ```
69 | # Build current code
70 | npm run build
71 | ```
72 |
73 | > Note: ensure your version of Node is 14 or higher to run scripts
74 |
75 | 8. Send your pull request:
76 |
77 | - Send your pull request to the `main` branch
78 | - Your pull request will be reviewed by the maintainers and the maintainers will decide if it is accepted or not
79 | - Once the pull request is accepted, the maintainers will merge it to the `main` branch
80 |
81 | ## Visual Changes
82 |
83 | When making a visual change, please provide screenshots
84 | and/or screencasts of the proposed change. This will help us to understand the
85 | desired change easier.
86 |
87 | ## Documentation
88 |
89 | If you want to make changes to existing pages on the documentation website (www.deca-ui.com),
90 | please create an issue that includes the URL of the page you want the change to occur on, and a brief
91 | description of what needs to be changed or documented
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
DecaUI
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | DecaUI provides a set of accessible and customizable React components that make it easy to quickly prototype and develop stunning websites.
15 |
16 | ## Getting Started
17 |
18 | ```
19 | npm install @deca-ui/react
20 | ```
21 |
22 | ## Using a component
23 |
24 | Here is a simple example of a basic app using DecaUI's `Button` component:
25 |
26 | ```jsx
27 | import { Button } from '@deca-ui/react';
28 |
29 | function App() {
30 | return Hello World ;
31 | }
32 | ```
33 |
34 | #### [Click here for the full documentation](https://www.deca-ui.com/docs/guide/installation)
35 |
36 | ## What's so different about DecaUI
37 |
38 | With DecaUI, developers can use the centralized theming system anywhere within their application with shorthand names for css properties.
39 |
40 | ### Custom CSS with other UI libraries
41 |
42 | ```jsx
43 |
51 |
52 |
53 |
58 | Create Account
59 |
60 |
61 | ```
62 |
63 | ### Custom CSS with DecaUI
64 |
65 | ```jsx
66 |
67 |
68 |
69 | Create Account
70 |
71 | ```
72 |
73 | ## Our focus is consistency
74 |
75 | The main problem with other UI libraries is that it's confusing to create consistent webpage layouts with them. DecaUI allows developers to utilize a root theme object which serves properties following the [System UI](https://github.com/system-ui/theme-specification) specification.
76 |
77 | ## Thank you React Status!
78 |
79 |
80 |
81 | Thanks to React Status for showcasing this project on their newsletter (issue #323 ).
82 |
83 |
84 |
--------------------------------------------------------------------------------
/config/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "exclude": [
4 | "../src/**/*.cy.tsx",
5 | "../src/**/*.test.tsx",
6 | "../src/**/*.stories.tsx"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/config/tsconfig.module.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.module.json",
3 | "exclude": [
4 | "../src/**/*.cy.tsx",
5 | "../src/**/*.test.tsx",
6 | "../src/**/*.stories.tsx"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress';
2 |
3 | export default defineConfig({
4 | projectId: 'udbcua',
5 | component: {
6 | devServer: {
7 | framework: 'react',
8 | bundler: 'vite',
9 | },
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/cypress.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { mount } from 'cypress/react';
3 | import '@testing-library/cypress/add-commands';
4 |
5 | export {};
6 |
7 | declare global {
8 | namespace Cypress {
9 | interface Chainable {
10 | baseMount: typeof mount;
11 | mount: typeof mount;
12 | darkMount: typeof mount;
13 | before(value: string): any;
14 | after(value: string): any;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const vite = require('vite');
3 |
4 | const cache = {};
5 |
6 | module.exports = (on, config) => {
7 | on('file:preprocessor', async (file) => {
8 | const { filePath, outputPath, shouldWatch } = file;
9 | if (cache[filePath]) {
10 | return cache[filePath];
11 | }
12 |
13 | const filename = path.basename(outputPath);
14 | const filenameWithoutExtension = path.basename(
15 | outputPath,
16 | path.extname(outputPath)
17 | );
18 |
19 | const viteConfig = {
20 | build: {
21 | emptyOutDir: false,
22 | minify: false,
23 | outDir: path.dirname(outputPath),
24 | sourcemap: true,
25 | write: true,
26 | },
27 | };
28 |
29 | if (filename.endsWith('.html')) {
30 | viteConfig.build.rollupOptions = {
31 | input: {
32 | [filenameWithoutExtension]: filePath,
33 | },
34 | };
35 | } else {
36 | viteConfig.build.lib = {
37 | entry: filePath,
38 | fileName: () => filename,
39 | formats: ['es'],
40 | name: filenameWithoutExtension,
41 | };
42 | }
43 |
44 | if (shouldWatch) {
45 | viteConfig.build.watch = true;
46 | }
47 |
48 | const watcher = await vite.build(viteConfig);
49 |
50 | if (shouldWatch) {
51 | watcher.on('event', (event) => {
52 | if (event.code === 'END') {
53 | file.emit('rerun');
54 | }
55 | });
56 | file.on('close', () => {
57 | delete cache[filePath];
58 | watcher.close();
59 | });
60 | }
61 |
62 | cache[filePath] = outputPath;
63 | return outputPath;
64 | });
65 |
66 | return config;
67 | };
68 |
--------------------------------------------------------------------------------
/cypress/support/commands.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import '@testing-library/cypress/add-commands';
3 |
--------------------------------------------------------------------------------
/cypress/support/component-index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Components App
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/cypress/support/component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Box from '../../src/lib/Box';
3 | import DecaUIProvider from '../../src/lib/Theme';
4 | import './commands';
5 | import { mount } from 'cypress/react18';
6 | import _cyp from '../../cypress';
7 |
8 | Cypress.Commands.add('mount', (component: React.ReactElement, options = {}) => {
9 | const wrapped = (
10 |
11 |
20 | {component}
21 |
22 |
23 | );
24 | return mount(wrapped, options);
25 | });
26 |
27 | Cypress.Commands.add(
28 | 'darkMount',
29 | (component: React.ReactElement, options = {}) => {
30 | const wrapped = (
31 |
32 |
41 | {component}
42 |
43 |
44 | );
45 | return mount(wrapped, options);
46 | }
47 | );
48 |
49 | Cypress.Commands.add('baseMount', mount);
50 |
51 | function unquote(str: string) {
52 | return str.replace(/(^")|("$)/g, '');
53 | }
54 |
55 | Cypress.Commands.add(
56 | 'before',
57 | {
58 | prevSubject: 'element',
59 | },
60 | (el, property) => {
61 | const win = el[0].ownerDocument.defaultView;
62 | const before = win.getComputedStyle(el[0], 'before'); return unquote(before.getPropertyValue(property));
63 | }
64 | );
65 |
66 | Cypress.Commands.add(
67 | 'after',
68 | {
69 | prevSubject: 'element',
70 | },
71 | (el, property) => {
72 | const win = el[0].ownerDocument.defaultView;
73 | const before = win.getComputedStyle(el[0], 'after');
74 | return unquote(before.getPropertyValue(property));
75 | }
76 | );
77 |
--------------------------------------------------------------------------------
/cypress/videos/Badge/Badge.cy.tsx.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Badge/Badge.cy.tsx.mp4
--------------------------------------------------------------------------------
/cypress/videos/Button/Button.cy.tsx.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Button/Button.cy.tsx.mp4
--------------------------------------------------------------------------------
/cypress/videos/Checkbox/Checkbox.cy.tsx.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Checkbox/Checkbox.cy.tsx.mp4
--------------------------------------------------------------------------------
/cypress/videos/Container/Container.cy.tsx.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Container/Container.cy.tsx.mp4
--------------------------------------------------------------------------------
/cypress/videos/Grid/Grid.cy.tsx.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Grid/Grid.cy.tsx.mp4
--------------------------------------------------------------------------------
/cypress/videos/Input/Input.cy.tsx.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Input/Input.cy.tsx.mp4
--------------------------------------------------------------------------------
/cypress/videos/Modal/Modal.cy.tsx.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Modal/Modal.cy.tsx.mp4
--------------------------------------------------------------------------------
/cypress/videos/Popover/Popover.cy.tsx.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Popover/Popover.cy.tsx.mp4
--------------------------------------------------------------------------------
/cypress/videos/Radio/Radio.cy.tsx.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Radio/Radio.cy.tsx.mp4
--------------------------------------------------------------------------------
/cypress/videos/Switch/Switch.cy.tsx.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Switch/Switch.cy.tsx.mp4
--------------------------------------------------------------------------------
/cypress/videos/Text/Text.cy.tsx.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/cypress/videos/Text/Text.cy.tsx.mp4
--------------------------------------------------------------------------------
/public/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deca-org/deca-ui/7ab900dbc64219d9f2ee17db3b6646793aa8247c/public/cover.png
--------------------------------------------------------------------------------
/setupTests.ts:
--------------------------------------------------------------------------------
1 | jest.mock('uuid', () => ({
2 | v4: jest.fn(() => 1),
3 | }));
4 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from '@lib/Badge';
2 | export * from '@lib/Button';
3 | export * from '@lib/Grid';
4 | export * from '@lib/Modal';
5 | export * from '@lib/Radio';
6 | export * from '@lib/Text';
7 | export * from '@lib/Utils';
8 | export * from '@lib/Box';
9 | export * from '@lib/Checkbox';
10 | export * from '@lib/Input';
11 | export * from '@lib/Popover';
12 | export * from '@lib/Switch';
13 | export * from '@lib/Theme';
14 | export * from '@lib/Container';
15 |
--------------------------------------------------------------------------------
/src/lib/Badge/Badge.cy.tsx:
--------------------------------------------------------------------------------
1 | import { Test } from '../Utils';
2 | import _cyp from '../../../cypress';
3 | import React from 'react';
4 | import Badge from './Badge';
5 | import { standardColors } from '../Theme';
6 |
7 | describe('components/Badge', () => {
8 | const selector = '[data-testid="test.badge"]';
9 | it('pill', () => {
10 | cy.mount(
11 |
12 | Label
13 |
14 | );
15 | cy.get(selector).should(
16 | 'have.css',
17 | 'border-radius',
18 | Test.borderRadius('pill')
19 | );
20 | });
21 | describe('sizes', () => {
22 | it('sm', () => {
23 | cy.mount(
24 |
25 | Label
26 |
27 | );
28 | cy.get(selector).should('have.css', 'fontSize', Test.fontSize('caption'));
29 | cy.get(selector).should('have.css', 'paddingTop', Test.space('1'));
30 | cy.get(selector).should('have.css', 'paddingBottom', Test.space('1'));
31 | cy.get(selector).should('have.css', 'paddingLeft', Test.space('2'));
32 | cy.get(selector).should('have.css', 'paddingRight', Test.space('2'));
33 | });
34 | it('md', () => {
35 | cy.mount(
36 |
37 | Label
38 |
39 | );
40 | cy.get(selector).should('have.css', 'fontSize', Test.fontSize('bodySm'));
41 | cy.get(selector).should('have.css', 'paddingTop', Test.space('1'));
42 | cy.get(selector).should('have.css', 'paddingBottom', Test.space('1'));
43 | cy.get(selector).should('have.css', 'paddingLeft', Test.space('3 - 1'));
44 | cy.get(selector).should('have.css', 'paddingRight', Test.space('3 - 1'));
45 | });
46 | it('lg', () => {
47 | cy.mount(
48 |
49 | Label
50 |
51 | );
52 | cy.get(selector).should('have.css', 'fontSize', Test.fontSize('body'));
53 | cy.get(selector).should('have.css', 'paddingTop', Test.space('1'));
54 | cy.get(selector).should('have.css', 'paddingBottom', Test.space('1'));
55 | cy.get(selector).should('have.css', 'paddingLeft', Test.space('3'));
56 | cy.get(selector).should('have.css', 'paddingRight', Test.space('3'));
57 | });
58 | });
59 | describe('color', () => {
60 | standardColors.map((color) => {
61 | it(color, () => {
62 | cy.mount(
63 |
64 | Label
65 |
66 | );
67 | cy.get(selector).should(
68 | 'have.css',
69 | 'background-color',
70 | Test.color(color)
71 | );
72 | cy.get(selector).should(
73 | 'have.css',
74 | 'color',
75 | Test.color(`${color}-readable`)
76 | );
77 | });
78 | });
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/src/lib/Badge/Badge.stories.tsx:
--------------------------------------------------------------------------------
1 | import { DecaUIProvider } from '@lib/Theme';
2 | import { ComponentMeta, ComponentStory } from '@storybook/react';
3 | import React from 'react';
4 |
5 | import Badge from './Badge';
6 |
7 | export default {
8 | title: 'Badge',
9 | component: Badge,
10 | } as ComponentMeta;
11 |
12 | const Template: ComponentStory = (args) => ;
13 |
14 | export const Default = Template.bind({});
15 | Default.args = {
16 | children: 'Hello',
17 | size: 'md',
18 | color: 'primary',
19 | pill: false,
20 | css: {},
21 | className: '',
22 | };
23 |
24 | export const WithTheme = Template.bind({});
25 |
26 | WithTheme.args = { ...Default.args };
27 | WithTheme.decorators = [
28 | (Story) => (
29 |
39 |
40 |
41 | ),
42 | ];
43 |
--------------------------------------------------------------------------------
/src/lib/Badge/Badge.styles.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@lib/Theme/stitches.config';
2 |
3 | export const StyledBadge = styled('span', {
4 | transition: '$default',
5 | fontFamily: '$normal',
6 | fontWeight: '$bold',
7 | variants: {
8 | pill: {
9 | true: {
10 | br: '$pill',
11 | },
12 | false: {
13 | br: '$sm',
14 | },
15 | },
16 | size: {
17 | sm: {
18 | fontSize: '$caption',
19 | py: '$1',
20 | px: '$2',
21 | },
22 | md: {
23 | fontSize: '$bodySm',
24 | py: '$1',
25 | paddingRight: 'calc($3 - $1)',
26 | paddingLeft: 'calc($3 - $1)',
27 | },
28 | lg: {
29 | fontSize: '$body',
30 | py: '$1',
31 | px: '$3',
32 | },
33 | },
34 | color: {
35 | primary: {
36 | bg: '$primary',
37 | color: '$primary-readable',
38 | },
39 | secondary: {
40 | bg: '$secondary',
41 | color: '$secondary-readable',
42 | },
43 | success: {
44 | bg: '$success',
45 | color: '$success-readable',
46 | },
47 | warning: {
48 | bg: '$warning',
49 | color: '$warning-readable',
50 | },
51 | error: {
52 | bg: '$error',
53 | color: '$error-readable',
54 | },
55 | },
56 | },
57 | });
58 |
--------------------------------------------------------------------------------
/src/lib/Badge/Badge.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import '@testing-library/jest-dom';
3 | import React from 'react';
4 |
5 | import Badge from './Badge';
6 |
7 | describe('components/Badge', () => {
8 | it('renders content', () => {
9 | const { getByText } = render(Label );
10 | expect(getByText('Label')).toBeInTheDocument();
11 | });
12 | it('matches snapshot', () => {
13 | const { asFragment } = render( );
14 | expect(asFragment()).toMatchSnapshot();
15 | });
16 | it('renders all colors', () => {
17 | const { asFragment } = render(
18 | <>
19 |
20 |
21 |
22 |
23 |
24 | >
25 | );
26 | expect(asFragment()).toMatchSnapshot();
27 | });
28 | it('renders all sizes', () => {
29 | const { asFragment } = render(
30 | <>
31 |
32 |
33 |
34 | >
35 | );
36 | expect(asFragment()).toMatchSnapshot();
37 | });
38 | it('renders pill option', () => {
39 | const { asFragment } = render( );
40 | expect(asFragment()).toMatchSnapshot();
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/src/lib/Badge/Badge.tsx:
--------------------------------------------------------------------------------
1 | import { CSS, StandardColors } from '@lib/Theme/stitches.config';
2 | import {
3 | __DEV__,
4 | PolymorphicRef,
5 | PolymorphicComponentPropWithRef,
6 | } from '@lib/Utils';
7 | import clsx from 'clsx';
8 | import React from 'react';
9 |
10 | import { StyledBadge } from './Badge.styles';
11 |
12 | /**
13 | * Badges are used to highlight an item's status for quick recognition.
14 | */
15 | interface Props {
16 | /**
17 | * The content of the component.
18 | */
19 | children?: React.ReactNode | undefined;
20 | /**
21 | * ClassName applied to the component.
22 | * @default ''
23 | */
24 | className?: string;
25 | /**
26 | * Color to use.
27 | * @default primary
28 | */
29 | color?: StandardColors;
30 | /**
31 | * Size of the component.
32 | * @default md
33 | */
34 | size?: 'sm' | 'md' | 'lg';
35 | /**
36 | * Override default CSS style.
37 | */
38 | css?: CSS;
39 | /**
40 | * Have component be pill shaped
41 | */
42 | pill?: boolean;
43 | }
44 |
45 | export type BadgeProps =
46 | PolymorphicComponentPropWithRef;
47 |
48 | export type BadgeComponent = ((
49 | props: BadgeProps
50 | ) => React.ReactElement | null) & { displayName?: string };
51 |
52 | const Badge: BadgeComponent = React.forwardRef(
53 | (
54 | {
55 | children,
56 | className,
57 | color = 'primary',
58 | size = 'md',
59 | as,
60 | css,
61 | pill = false,
62 | ...props
63 | }: BadgeProps,
64 | ref?: PolymorphicRef
65 | ) => {
66 | const preClass = 'decaBadge';
67 |
68 | return (
69 |
79 | {children}
80 |
81 | );
82 | }
83 | );
84 |
85 | if (__DEV__) {
86 | Badge.displayName = 'DecaUI.Badge';
87 | }
88 |
89 | export default Badge;
90 |
--------------------------------------------------------------------------------
/src/lib/Badge/__snapshots__/Badge.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`components/Badge matches snapshot 1`] = `
4 |
5 |
8 |
9 | `;
10 |
11 | exports[`components/Badge renders all colors 1`] = `
12 |
13 |
16 |
19 |
22 |
25 |
28 |
29 | `;
30 |
31 | exports[`components/Badge renders all sizes 1`] = `
32 |
33 |
36 |
39 |
42 |
43 | `;
44 |
45 | exports[`components/Badge renders pill option 1`] = `
46 |
47 |
50 |
51 | `;
52 |
--------------------------------------------------------------------------------
/src/lib/Badge/index.ts:
--------------------------------------------------------------------------------
1 | import Badge from './Badge';
2 | export * from './Badge';
3 | export { Badge };
4 | export default Badge;
5 |
--------------------------------------------------------------------------------
/src/lib/Box/Box.stories.tsx:
--------------------------------------------------------------------------------
1 | import Text from '@lib/Text';
2 | import { ComponentMeta, ComponentStory } from '@storybook/react';
3 | import React from 'react';
4 |
5 | import Box from './Box';
6 |
7 | export default {
8 | title: 'Box',
9 | component: Box,
10 | } as ComponentMeta;
11 |
12 | const Template: ComponentStory = (args) => ;
13 |
14 | const showcaseText = 'Almost before we knew it, we had left the ground.';
15 |
16 | export const Default = Template.bind({});
17 |
18 | Default.args = {
19 | css: {
20 | br: '$lg',
21 | size: '$25',
22 | linearGradient: '20deg, $pink900, $red500',
23 | },
24 | };
25 |
26 | export const WithChildren = () => (
27 |
38 | {showcaseText}
39 |
40 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eleifend
41 | rhoncus ligula, eget porta enim aliquam nec. Suspendisse ultrices lorem
42 | lobortis feugiat tempor.
43 |
44 |
45 | );
46 |
--------------------------------------------------------------------------------
/src/lib/Box/Box.styles.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@lib/Theme/stitches.config';
2 |
3 | export const StyledBox = styled('div', {
4 | fontFamily: '$normal',
5 | });
6 |
--------------------------------------------------------------------------------
/src/lib/Box/Box.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import '@testing-library/jest-dom';
3 | import React from 'react';
4 |
5 | import Box from './Box';
6 |
7 | describe('components/Box', () => {
8 | it('renders content', () => {
9 | const { getByText } = render(Children );
10 | expect(getByText('Children')).toBeInTheDocument();
11 | });
12 | it('matches snapshot', () => {
13 | const { asFragment } = render( );
14 | expect(asFragment()).toMatchSnapshot();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/lib/Box/Box.tsx:
--------------------------------------------------------------------------------
1 | import { CSS } from '@lib/Theme/stitches.config';
2 | import {
3 | __DEV__,
4 | PolymorphicRef,
5 | PolymorphicComponentPropWithRef,
6 | } from '@lib/Utils';
7 | import clsx from 'clsx';
8 | import React from 'react';
9 |
10 | import { StyledBox } from './Box.styles';
11 |
12 | /**
13 | * The Box component serves as a wrapper component
14 | */
15 | interface Props {
16 | /**
17 | * Override default CSS style.
18 | */
19 | css?: CSS;
20 | /**
21 | * The content of the component.
22 | */
23 | children?: React.ReactNode | undefined;
24 | /**
25 | * ClassName applied to the component.
26 | * @default ''
27 | */
28 | className?: string;
29 | }
30 |
31 | export type BoxProps =
32 | PolymorphicComponentPropWithRef;
33 |
34 | export type BoxComponent = ((
35 | props: BoxProps
36 | ) => React.ReactElement | null) & { displayName?: string };
37 |
38 | const Box: BoxComponent = React.forwardRef(
39 | (
40 | { as, css, children, className = '', ...boxProps }: BoxProps,
41 | ref?: PolymorphicRef
42 | ) => {
43 | const preClass = 'decaBox';
44 |
45 | return (
46 |
53 | {children}
54 |
55 | );
56 | }
57 | );
58 |
59 | if (__DEV__) {
60 | Box.displayName = 'DecaUI.Box';
61 | }
62 |
63 | export default Box;
64 |
--------------------------------------------------------------------------------
/src/lib/Box/__snapshots__/Box.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`components/Box matches snapshot 1`] = `
4 |
5 |
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/src/lib/Box/index.ts:
--------------------------------------------------------------------------------
1 | import Box from './Box';
2 | export * from './Box';
3 | export { Box };
4 | export default Box;
5 |
--------------------------------------------------------------------------------
/src/lib/Button/Button.stories.tsx:
--------------------------------------------------------------------------------
1 | import { DecaUIProvider } from '@lib/Theme';
2 | import { ComponentMeta, ComponentStory } from '@storybook/react';
3 | import { PurchaseTagAlt } from '@styled-icons/boxicons-solid/PurchaseTagAlt';
4 | import React from 'react';
5 |
6 | import Button from './Button';
7 |
8 | export default {
9 | title: 'Button',
10 | component: Button,
11 | } as ComponentMeta;
12 |
13 | const Template: ComponentStory = (args) => ;
14 |
15 | export const Default = Template.bind({});
16 |
17 | Default.args = {
18 | children: 'Button',
19 | color: 'primary',
20 | disabled: false,
21 | css: {},
22 | className: '',
23 | maxWidth: false,
24 | variant: 'solid',
25 | role: 'button',
26 | size: 'md',
27 | pill: false,
28 | };
29 |
30 | export const IconButton = Template.bind({});
31 | IconButton.args = {
32 | ...Default.args,
33 | children: undefined,
34 | icon: ,
35 | };
36 |
37 | export const IconWithLabel = Template.bind({});
38 | IconWithLabel.args = {
39 | ...Default.args,
40 | icon: ,
41 | };
42 |
43 | export const RightSideIconWithLabel = Template.bind({});
44 | RightSideIconWithLabel.args = {
45 | ...Default.args,
46 | iconRight: ,
47 | };
48 |
49 | export const WithTheme = Template.bind({});
50 |
51 | WithTheme.args = { ...Default.args };
52 | WithTheme.decorators = [
53 | (Story) => (
54 |
64 |
65 |
66 | ),
67 | ];
68 |
69 | export const DarkMode = Template.bind({});
70 |
71 | DarkMode.args = { ...Default.args };
72 | DarkMode.parameters = { backgrounds: { default: 'dark' } };
73 |
74 | DarkMode.decorators = [
75 | (Story) => {
76 | return (
77 |
78 |
79 |
80 | );
81 | },
82 | ];
83 |
--------------------------------------------------------------------------------
/src/lib/Button/Button.test.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowCircleRightOutline } from '@styled-icons/evaicons-outline/ArrowCircleRightOutline';
2 | import '@testing-library/jest-dom';
3 | import { render, fireEvent } from '@testing-library/react';
4 | import React from 'react';
5 |
6 | import Button from './Button';
7 |
8 | describe('components/Button', () => {
9 | it('renders content', () => {
10 | const { getByText } = render(Label );
11 | expect(getByText('Label')).toBeInTheDocument();
12 | });
13 |
14 | it('matches snapshot', () => {
15 | const { asFragment } = render(Label );
16 | expect(asFragment()).toMatchSnapshot();
17 | });
18 |
19 | it('onClick event fires', () => {
20 | const mockFn = jest.fn();
21 | const { getByText } = render(Label );
22 | fireEvent.click(getByText('Label'));
23 | expect(mockFn.mock.calls.length).toBe(1);
24 | });
25 |
26 | it('renders all colors', () => {
27 | const { asFragment } = render(
28 | <>
29 | Label
30 | Label
31 | Label
32 | Label
33 | Label
34 | >
35 | );
36 | expect(asFragment()).toMatchSnapshot();
37 | });
38 |
39 | it('renders all sizes', () => {
40 | const { asFragment } = render(
41 | <>
42 | Label
43 | Label
44 | Label
45 | >
46 | );
47 | expect(asFragment()).toMatchSnapshot();
48 | });
49 |
50 | it('renders all variants', () => {
51 | const { asFragment } = render(
52 | <>
53 | Label
54 | Label
55 | Label
56 | Label
57 | >
58 | );
59 | expect(asFragment()).toMatchSnapshot();
60 | });
61 |
62 | it('should ignore events when disabled', () => {
63 | const mockFn = jest.fn();
64 | const { getByText } = render(
65 |
66 | Label
67 |
68 | );
69 | fireEvent.click(getByText('Label'));
70 | expect(mockFn.mock.calls.length).toBe(0);
71 | });
72 |
73 | it('renders icon', () => {
74 | const { container, getByText } = render(
75 | }>Label
76 | );
77 | const iconEl = container.querySelector('svg');
78 | expect(iconEl).toBeInTheDocument();
79 | expect(getByText('Label')).toBeInTheDocument();
80 | });
81 |
82 | it('renders icon on right side', () => {
83 | const { container, getByText } = render(
84 | }>Label
85 | );
86 | const iconEl = container.querySelector('svg');
87 | expect(iconEl).toBeInTheDocument();
88 | expect(getByText('Label')).toBeInTheDocument();
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/src/lib/Button/ButtonIcon.tsx:
--------------------------------------------------------------------------------
1 | import { CSS, styled } from '@lib/Theme/stitches.config';
2 | import React from 'react';
3 |
4 | const StyledButtonIcon = styled('span', {
5 | display: 'flex',
6 | justifyContent: 'center',
7 | alignItems: 'center',
8 | height: '100%',
9 | '& svg': {
10 | background: 'transparent',
11 | },
12 | compoundVariants: [
13 | {
14 | isSingle: false,
15 | side: 'left',
16 | css: {
17 | mr: '$1',
18 | },
19 | },
20 | {
21 | isSingle: false,
22 | side: 'right',
23 | css: {
24 | ml: '$1',
25 | },
26 | },
27 | {
28 | isSingle: true,
29 | size: 'sm',
30 | css: {
31 | '& svg': {
32 | height: 'calc(100% - $0)',
33 | width: '100%',
34 | },
35 | },
36 | },
37 | ],
38 | variants: {
39 | size: {
40 | sm: {
41 | '& svg': {
42 | height: 'calc(100% - $1)',
43 | width: '100%',
44 | },
45 | },
46 | md: {
47 | '& svg': {
48 | height: 'calc(100% - $2)',
49 | width: '100%',
50 | },
51 | },
52 | lg: {
53 | '& svg': {
54 | height: 'calc(100% - $3)',
55 | width: '100%',
56 | },
57 | },
58 | },
59 | isSingle: {
60 | true: {
61 | '& svg': {
62 | height: 'calc(100% - $2)',
63 | width: '100%',
64 | },
65 | },
66 | false: {},
67 | },
68 | side: {
69 | left: {},
70 | right: {},
71 | },
72 | },
73 | });
74 |
75 | export interface ButtonIconProps {
76 | children: React.ReactNode | undefined;
77 | className?: string;
78 | css?: CSS;
79 | size?: 'sm' | 'md' | 'lg';
80 | isSingle?: boolean;
81 | side?: 'left' | 'right';
82 | }
83 |
84 | const ButtonIcon = ({
85 | children,
86 | className,
87 | css,
88 | size,
89 | isSingle,
90 | side,
91 | ...props
92 | }: ButtonIconProps) => (
93 |
101 | {children}
102 |
103 | );
104 |
105 | export default ButtonIcon;
106 |
--------------------------------------------------------------------------------
/src/lib/Button/__snapshots__/Button.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`components/Button matches snapshot 1`] = `
4 |
5 |
9 | Label
10 |
11 |
12 | `;
13 |
14 | exports[`components/Button renders all colors 1`] = `
15 |
16 |
20 | Label
21 |
22 |
26 | Label
27 |
28 |
32 | Label
33 |
34 |
38 | Label
39 |
40 |
44 | Label
45 |
46 |
47 | `;
48 |
49 | exports[`components/Button renders all sizes 1`] = `
50 |
51 |
55 | Label
56 |
57 |
61 | Label
62 |
63 |
67 | Label
68 |
69 |
70 | `;
71 |
72 | exports[`components/Button renders all variants 1`] = `
73 |
74 |
78 | Label
79 |
80 |
84 | Label
85 |
86 |
90 | Label
91 |
92 |
96 | Label
97 |
98 |
99 | `;
100 |
--------------------------------------------------------------------------------
/src/lib/Button/index.ts:
--------------------------------------------------------------------------------
1 | import Button from './Button';
2 | export * from './Button';
3 | export { Button };
4 | export default Button;
5 |
--------------------------------------------------------------------------------
/src/lib/Checkbox/Checkbox.cy.tsx:
--------------------------------------------------------------------------------
1 | import { standardColors } from '../Theme';
2 | import { Test } from '../Utils';
3 | import _cyp from '../../../cypress';
4 | import React from 'react';
5 | import Checkbox from './Checkbox';
6 |
7 | describe('components/Checkbox', () => {
8 | describe('before click', () => {
9 | it('border-color', () => {
10 | cy.mount( );
11 |
12 | cy.get('label')
13 | .before('border-color')
14 | .should('eq', Test.color('gray600'));
15 | });
16 | it('disabled', () => {
17 | cy.mount( );
18 |
19 | cy.get('label').should('have.css', 'color', Test.color('gray500'));
20 |
21 | cy.get('label')
22 | .before('border-color')
23 | .should('eq', Test.color('gray400'));
24 | });
25 | });
26 | describe('colors', () => {
27 | it('label color', () => {
28 | cy.mount( );
29 | cy.get('label').should('have.css', 'color', Test.color('black'));
30 | });
31 | standardColors.map((color) => {
32 | describe(color, () => {
33 | it('background-color', () => {
34 | cy.mount( );
35 |
36 | cy.get('label').click();
37 |
38 | // wait for css transition to finish
39 | cy.wait(250);
40 | cy.get('label')
41 | .before('background-color')
42 | .should('eq', Test.color(color));
43 | });
44 | it('border-color', () => {
45 | cy.mount( );
46 |
47 | cy.get('label').click();
48 |
49 | // wait for css transition to finish
50 | cy.wait(250);
51 | cy.get('label')
52 | .before('border-color')
53 | .should('eq', Test.color(color));
54 | });
55 | it('disabled', () => {
56 | cy.mount(
57 |
58 | );
59 |
60 | cy.get('label').before('opacity').should('eq', '0.55');
61 | cy.get('svg').should('have.css', 'opacity', '0.9');
62 | });
63 | });
64 | });
65 | });
66 | describe('sizes', () => {
67 | it('sm', () => {
68 | cy.mount( );
69 | cy.get('label').before('width').should('eq', Test.size('2'));
70 | cy.get('label').before('height').should('eq', Test.size('2'));
71 | cy.get('label').before('margin-right').should('eq', Test.space('1'));
72 | cy.get('label').should('have.css', 'font-size', Test.fontSize('caption'));
73 | cy.get('svg').should('have.css', 'width', Test.size('1'));
74 | });
75 | it('md', () => {
76 | cy.mount( );
77 | cy.get('label').before('width').should('eq', Test.size('3'));
78 | cy.get('label').before('height').should('eq', Test.size('3'));
79 | cy.get('label').before('margin-right').should('eq', Test.space('2'));
80 | cy.get('label').should('have.css', 'font-size', Test.fontSize('bodySm'));
81 | cy.get('svg').should('have.css', 'width', Test.size('2'));
82 | });
83 | it('lg', () => {
84 | cy.mount( );
85 | cy.get('label').before('width').should('eq', Test.size('4'));
86 | cy.get('label').before('height').should('eq', Test.size('4'));
87 | cy.get('label').before('margin-right').should('eq', Test.space('2'));
88 | cy.get('label').should('have.css', 'font-size', Test.fontSize('body'));
89 | cy.get('svg').should('have.css', 'width', Test.size('3'));
90 | });
91 | });
92 |
93 | describe('no label', () => {
94 | describe('should have no margin', () => {
95 | it('sm', () => {
96 | cy.mount( );
97 | cy.get('label').before('margin-right').should('eq', '0px');
98 | });
99 | it('md', () => {
100 | cy.mount( );
101 | cy.get('label').before('margin-right').should('eq', '0px');
102 | });
103 | it('sm', () => {
104 | cy.mount( );
105 | cy.get('label').before('margin-right').should('eq', '0px');
106 | });
107 | });
108 | });
109 |
110 | describe('dark mode', () => {
111 | it('label color', () => {
112 | cy.darkMount( );
113 | cy.get('label').should('have.css', 'color', Test.color('white'));
114 | });
115 |
116 | it('disabled state', () => {
117 | cy.darkMount( );
118 | cy.get('label').before('opacity').should('eq', '0.5');
119 | cy.get('svg').should('have.css', 'opacity', '0.3');
120 | });
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/src/lib/Checkbox/Checkbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import { DecaUIProvider } from '@lib/Theme';
2 | import { ComponentMeta, ComponentStory } from '@storybook/react';
3 | import React from 'react';
4 |
5 | import Checkbox from './Checkbox';
6 |
7 | export default {
8 | title: 'Checkbox',
9 | component: Checkbox,
10 | } as ComponentMeta;
11 |
12 | const Template: ComponentStory = (args) => (
13 |
14 | );
15 |
16 | export const Default = Template.bind({});
17 | Default.args = {
18 | label: 'Label',
19 | size: 'md',
20 | color: 'primary',
21 | disabled: false,
22 | css: {},
23 | className: '',
24 | initialCheck: true,
25 | required: false,
26 | };
27 |
28 | export const NoLabel = Template.bind({});
29 | NoLabel.args = {
30 | ...Default.args,
31 | label: '',
32 | };
33 |
34 | export const WithTheme = Template.bind({});
35 |
36 | WithTheme.args = { ...Default.args };
37 | WithTheme.decorators = [
38 | (Story) => (
39 |
46 |
47 |
48 | ),
49 | ];
50 |
51 | export const DarkMode = Template.bind({});
52 |
53 | DarkMode.args = { ...Default.args };
54 | DarkMode.parameters = { backgrounds: { default: 'dark' } };
55 |
56 | DarkMode.decorators = [
57 | (Story) => (
58 |
59 |
60 |
61 | ),
62 | ];
63 |
--------------------------------------------------------------------------------
/src/lib/Checkbox/Checkbox.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, act } from '@testing-library/react';
2 | import React from 'react';
3 |
4 | import Checkbox from './Checkbox';
5 |
6 | describe('components/Checkbox', () => {
7 | it('matches screenshot', () => {
8 | const { asFragment } = render( );
9 | expect(asFragment()).toMatchSnapshot();
10 | });
11 | it('onChange event fires', () => {
12 | const mockFn = jest.fn();
13 | const utils = render( );
14 | const input = utils.getByLabelText('Label Text');
15 | act(() => {
16 | input.click();
17 | });
18 | expect(mockFn.mock.calls.length).toBe(1);
19 | });
20 | it('should ignore events when disabled', () => {
21 | const mockFn = jest.fn();
22 | const utils = render(
23 |
24 | );
25 | const input = utils.getByLabelText('Label Text');
26 | act(() => {
27 | input.click();
28 | });
29 | expect(mockFn.mock.calls.length).toBe(0);
30 | });
31 | it('renders all colors', () => {
32 | const { asFragment } = render(
33 | <>
34 |
35 |
36 |
37 |
38 |
39 | >
40 | );
41 | expect(asFragment()).toMatchSnapshot();
42 | });
43 | it('renders all sizes', () => {
44 | const { asFragment } = render(
45 | <>
46 |
47 |
48 |
49 | >
50 | );
51 | expect(asFragment()).toMatchSnapshot();
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/src/lib/Checkbox/CheckboxGroup/CheckboxGroup.stories.tsx:
--------------------------------------------------------------------------------
1 | import Checkbox from '@lib/Checkbox';
2 | import { DecaUIProvider } from '@lib/Theme';
3 | import { ComponentMeta, ComponentStory } from '@storybook/react';
4 | import React from 'react';
5 |
6 | export default {
7 | title: 'CheckboxGroup',
8 | component: Checkbox.Group,
9 | } as ComponentMeta;
10 |
11 | const Template: ComponentStory = (args) => (
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 | export const Default = Template.bind({});
21 | Default.args = {
22 | defaultValue: ['A', 'B'],
23 | name: 'FormGroup-Checkbox',
24 | disabled: false,
25 | className: '',
26 | color: 'primary',
27 | };
28 |
29 | export const SingleDisabled = () => (
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 |
38 | export const WithTheme = Template.bind({});
39 |
40 | WithTheme.args = { ...Default.args };
41 | WithTheme.decorators = [
42 | (Story) => (
43 |
50 |
51 |
52 | ),
53 | ];
54 |
55 | export const DarkMode = Template.bind({});
56 |
57 | DarkMode.args = { ...Default.args };
58 | DarkMode.parameters = { backgrounds: { default: 'dark' } };
59 |
60 | DarkMode.decorators = [
61 | (Story) => (
62 |
63 |
64 |
65 | ),
66 | ];
67 |
--------------------------------------------------------------------------------
/src/lib/Checkbox/CheckboxGroup/CheckboxGroup.styles.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@lib/Theme/stitches.config';
2 |
3 | export const StyledCheckboxGroupWrapper = styled('div', {
4 | position: 'relative',
5 | boxSizing: 'border-box',
6 | display: 'flex',
7 | flexDirection: 'column',
8 | gap: '$2',
9 | });
10 |
--------------------------------------------------------------------------------
/src/lib/Checkbox/CheckboxGroup/CheckboxGroup.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import Checkbox from '@lib/Checkbox';
3 | import { render, screen } from '@testing-library/react';
4 | import userEvent from '@testing-library/user-event';
5 | import React from 'react';
6 |
7 | describe('components/CheckboxGroup', () => {
8 | it('matches snapshot', () => {
9 | const { asFragment } = render(
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | expect(asFragment()).toMatchSnapshot();
18 | });
19 | it('works as an uncontrolled component', async () => {
20 | const user = userEvent.setup();
21 | render(
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 |
30 | expect(screen.getByLabelText('Option A')).toBeChecked();
31 | expect(screen.getByLabelText('Option B')).toBeChecked();
32 | await user.click(screen.getByLabelText('Option C'));
33 | expect(screen.getByLabelText('Option C')).toBeChecked();
34 | });
35 | it('works as a controlled component', async () => {
36 | let value = ['A', 'B'];
37 | const user = userEvent.setup();
38 |
39 | render(
40 | ) =>
43 | (value = [...value, e.target.value])
44 | }
45 | >
46 |
47 |
48 |
49 |
50 |
51 | );
52 |
53 | expect(screen.getByLabelText('Option A')).toBeChecked();
54 | expect(screen.getByLabelText('Option B')).toBeChecked();
55 | await user.click(screen.getByLabelText('Option C'));
56 | expect(value).toStrictEqual(['A', 'B', 'C']);
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/src/lib/Checkbox/CheckboxGroup/CheckboxGroup.tsx:
--------------------------------------------------------------------------------
1 | import { CheckboxProps } from '@lib/Checkbox';
2 | import { CSS, StandardColors } from '@lib/Theme/stitches.config';
3 | import {
4 | PolymorphicRef,
5 | PolymorphicComponentPropWithRef,
6 | uuid,
7 | __DEV__,
8 | } from '@lib/Utils';
9 | import clsx from 'clsx';
10 | import React, { useMemo } from 'react';
11 | import { StyledCheckboxGroupWrapper } from './CheckboxGroup.styles';
12 |
13 | /**
14 | * CheckboxGroup is a helpful wrapper used to group checkbox components.
15 | */
16 | interface Props {
17 | /**
18 | * The content of the component.
19 | */
20 | children?:
21 | | Array>>
22 | | React.ReactElement>;
23 | /**
24 | * Array of checkboxes that are selected by default. Used when component is not controlled.
25 | */
26 | defaultValue?: Array;
27 | /**
28 | * ClassName applied to the component.
29 | * @default ''
30 | */
31 | className?: string;
32 | /**
33 | * The name used to reference the value of the control. If you do not provide this prop, it falls back to a randomly generated name.
34 | */
35 | name?: string;
36 | /**
37 | * Callback fired when a checkbox is selected.
38 | */
39 | onChange?(e: React.ChangeEvent): void;
40 | /**
41 | * Array of selected checkboxes. Used when component is controlled.
42 | */
43 | value?: Array;
44 | /**
45 | * Apply disabled state to all checkboxes in the checkbox group component
46 | * @default false
47 | */
48 | disabled?: boolean;
49 | /**
50 | * Override default CSS style.
51 | */
52 | css?: CSS;
53 | /**
54 | * Color of checkboxes when active.
55 | */
56 | color?: StandardColors;
57 | /**
58 | * Size of each checkbox.
59 | */
60 | size?: 'sm' | 'md' | 'lg';
61 | }
62 |
63 | export type CheckboxGroupProps =
64 | PolymorphicComponentPropWithRef;
65 |
66 | export type CheckboxGroupComponent = ((
67 | props: CheckboxGroupProps
68 | ) => React.ReactElement | null) & { displayName?: string };
69 |
70 | const CheckboxGroup: CheckboxGroupComponent = React.forwardRef(
71 | (
72 | {
73 | children,
74 | defaultValue,
75 | className = '',
76 | name,
77 | onChange,
78 | value,
79 | disabled = false,
80 | as,
81 | css,
82 | color,
83 | size,
84 | ...props
85 | }: CheckboxGroupProps,
86 | ref?: PolymorphicRef
87 | ) => {
88 | const presetId = uuid('checkbox');
89 |
90 | const getName = useMemo(() => {
91 | if (name) {
92 | return name;
93 | }
94 | return presetId;
95 | }, [name]);
96 |
97 | const preClass = 'decaCheckboxGroup';
98 |
99 | return (
100 |
107 | {React.Children.map(
108 | children as React.ReactElement>,
109 | (child: React.ReactElement>) => {
110 | return React.cloneElement(child, {
111 | name: getName,
112 | onChange,
113 | initialCheck:
114 | defaultValue &&
115 | defaultValue.includes(child.props.value as string),
116 | checked: value && value.includes(child.props.value as string),
117 | disabled: disabled ? disabled : child.props.disabled,
118 | color,
119 | size,
120 | });
121 | }
122 | )}
123 |
124 | );
125 | }
126 | );
127 |
128 | if (__DEV__) {
129 | CheckboxGroup.displayName = 'DecaUI.CheckboxGroup';
130 | }
131 |
132 | export default CheckboxGroup;
133 |
--------------------------------------------------------------------------------
/src/lib/Checkbox/CheckboxGroup/__snapshots__/CheckboxGroup.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`components/CheckboxGroup matches snapshot 1`] = `
4 |
5 |
8 |
11 |
19 |
23 |
30 |
34 |
35 | Option A
36 |
37 |
38 |
41 |
49 |
53 |
60 |
64 |
65 | Option B
66 |
67 |
68 |
71 |
78 |
82 |
89 |
93 |
94 | Option C
95 |
96 |
97 |
100 |
107 |
111 |
118 |
122 |
123 | Option D
124 |
125 |
126 |
127 |
128 | `;
129 |
--------------------------------------------------------------------------------
/src/lib/Checkbox/CheckboxGroup/index.ts:
--------------------------------------------------------------------------------
1 | import CheckboxGroup from './CheckboxGroup';
2 | export * from './CheckboxGroup';
3 | export { CheckboxGroup };
4 | export default CheckboxGroup;
5 |
--------------------------------------------------------------------------------
/src/lib/Checkbox/index.ts:
--------------------------------------------------------------------------------
1 | import Checkbox from './Checkbox';
2 | import CheckboxGroup from './CheckboxGroup';
3 |
4 | Checkbox.Group = CheckboxGroup;
5 |
6 | export * from './Checkbox';
7 | export { Checkbox };
8 | export default Checkbox;
9 |
--------------------------------------------------------------------------------
/src/lib/Container/Container.cy.tsx:
--------------------------------------------------------------------------------
1 | import { Test } from '../Utils';
2 | import _cyp from '../../../cypress';
3 | import React from 'react';
4 | import Container from './Container';
5 | import Box from '../Box';
6 |
7 | const ContainerItem = () => (
8 |
17 | Content
18 |
19 | );
20 |
21 | describe('components/Container', () => {
22 | const selector = '[data-testid="test.container"]';
23 | it('fluid', () => {
24 | cy.baseMount(
25 |
26 |
27 |
28 | );
29 | cy.get(selector).should('have.css', 'max-width', '100%');
30 | });
31 | describe('px', () => {
32 | it('none', () => {
33 | cy.baseMount(
34 |
35 |
36 |
37 | );
38 | cy.get(selector).should('have.css', 'padding-left', Test.space('n'));
39 | cy.get(selector).should('have.css', 'padding-right', Test.space('n'));
40 | });
41 | it('sm', () => {
42 | cy.baseMount(
43 |
44 |
45 |
46 | );
47 | cy.get(selector).should('have.css', 'padding-left', Test.space('2'));
48 | cy.get(selector).should('have.css', 'padding-right', Test.space('2'));
49 | });
50 | it('md', () => {
51 | cy.baseMount(
52 |
53 |
54 |
55 | );
56 | cy.get(selector).should('have.css', 'padding-left', Test.space('3'));
57 | cy.get(selector).should('have.css', 'padding-right', Test.space('3'));
58 | });
59 | it('lg', () => {
60 | cy.baseMount(
61 |
62 |
63 |
64 | );
65 | cy.get(selector).should('have.css', 'padding-left', Test.space('4'));
66 | cy.get(selector).should('have.css', 'padding-right', Test.space('4'));
67 | });
68 | });
69 | describe('breakpoints', () => {
70 | it('xs', () => {
71 | cy.viewport(300, 500);
72 | cy.baseMount(
73 |
74 |
75 |
76 | );
77 | cy.get(selector).should(
78 | 'have.css',
79 | 'max-width',
80 | Test.breakpoint('xs') + 'px'
81 | );
82 | });
83 | it('sm', () => {
84 | cy.viewport(Test.breakpoint('sm'), 500);
85 | cy.baseMount(
86 |
87 |
88 |
89 | );
90 | cy.get(selector).should(
91 | 'have.css',
92 | 'max-width',
93 | Test.breakpoint('sm') + 'px'
94 | );
95 | });
96 | it('md', () => {
97 | cy.viewport(Test.breakpoint('md'), 500);
98 | cy.baseMount(
99 |
100 |
101 |
102 | );
103 | cy.get(selector).should(
104 | 'have.css',
105 | 'max-width',
106 | Test.breakpoint('sm') + 'px'
107 | );
108 | });
109 | it('lg', () => {
110 | cy.viewport(Test.breakpoint('lg'), 500);
111 | cy.baseMount(
112 |
113 |
114 |
115 | );
116 | cy.get(selector).should(
117 | 'have.css',
118 | 'max-width',
119 | Test.breakpoint('md') + 'px'
120 | );
121 | });
122 | it('xl', () => {
123 | cy.viewport(Test.breakpoint('xl'), 500);
124 | cy.baseMount(
125 |
126 |
127 |
128 | );
129 | cy.get(selector).should(
130 | 'have.css',
131 | 'max-width',
132 | Test.breakpoint('lg') + 'px'
133 | );
134 | });
135 | });
136 | });
137 |
--------------------------------------------------------------------------------
/src/lib/Container/Container.stories.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@lib/Box';
2 | import { ComponentMeta, ComponentStory } from '@storybook/react';
3 | import React from 'react';
4 |
5 | import Container from './Container';
6 |
7 | export default {
8 | title: 'Container',
9 | component: Container,
10 | } as ComponentMeta;
11 |
12 | const Template: ComponentStory = (args) => (
13 |
14 |
22 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris ipsum
23 | justo, interdum eget lacus sit amet, consequat scelerisque lectus.
24 | Suspendisse vel tincidunt purus.
25 |
26 |
27 | );
28 |
29 | export const Responsive = Template.bind({});
30 |
31 | Responsive.args = {
32 | responsive: true,
33 | px: 'sm',
34 | fluid: false,
35 | };
36 |
37 | export const Fluid = Template.bind({});
38 |
39 | Fluid.args = {
40 | responsive: true,
41 | px: 'sm',
42 | fluid: true,
43 | };
44 |
--------------------------------------------------------------------------------
/src/lib/Container/Container.styles.ts:
--------------------------------------------------------------------------------
1 | import { styled } from '@lib/Theme/stitches.config';
2 |
3 | export const StyledContainer = styled('div', {
4 | w: '100%',
5 | mr: 'auto',
6 | ml: 'auto',
7 | variants: {
8 | px: {
9 | none: {
10 | px: '$n',
11 | },
12 | sm: {
13 | px: '$2',
14 | },
15 | md: {
16 | px: '$3',
17 | },
18 | lg: {
19 | px: '$4',
20 | },
21 | },
22 | responsive: {
23 | true: {
24 | '@n': {
25 | maxWidth: '$breakpoints$xs',
26 | },
27 | '@xs': {
28 | maxWidth: '$breakpoints$xs',
29 | },
30 | '@sm': {
31 | maxWidth: '$breakpoints$sm',
32 | },
33 | '@md': {
34 | maxWidth: '$breakpoints$sm',
35 | },
36 | '@lg': {
37 | maxWidth: '$breakpoints$md',
38 | },
39 | '@xl': {
40 | maxWidth: '$breakpoints$lg',
41 | },
42 | },
43 | },
44 | fluid: {
45 | true: {
46 | maxWidth: '100%',
47 | },
48 | },
49 | },
50 | });
51 |
--------------------------------------------------------------------------------
/src/lib/Container/Container.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import '@testing-library/jest-dom';
3 | import React from 'react';
4 |
5 | import Container from './Container';
6 |
7 | describe('components/Container', () => {
8 | it('renders content', () => {
9 | const { getByText } = render(Children );
10 | expect(getByText('Children')).toBeInTheDocument();
11 | });
12 | it('matches snapshot', () => {
13 | const { asFragment } = render( );
14 | expect(asFragment()).toMatchSnapshot();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/lib/Container/Container.tsx:
--------------------------------------------------------------------------------
1 | import { CSS } from '@lib/Theme/stitches.config';
2 | import {
3 | PolymorphicRef,
4 | PolymorphicComponentPropWithRef,
5 | __DEV__,
6 | } from '@lib/Utils';
7 | import clsx from 'clsx';
8 | import React from 'react';
9 |
10 | import { StyledContainer } from './Container.styles';
11 |
12 | /**
13 | * The Container component fixes an element's width to the current breakpoint
14 | */
15 | interface Props {
16 | /**
17 | * Override default CSS style.
18 | */
19 | css?: CSS;
20 | /**
21 | * The content of the component.
22 | */
23 | children?: React.ReactNode | undefined;
24 | /**
25 | * ClassName applied to the component.
26 | * @default ''
27 | */
28 | className?: string;
29 | /**
30 | * padding applied on each side
31 | * @default sm
32 | */
33 | px?: 'none' | 'sm' | 'md' | 'lg';
34 | /**
35 | * container max-width changes with breakpoint
36 | * @default true
37 | */
38 | responsive?: boolean;
39 | /**
40 | * max-width is set to 100%
41 | */
42 | fluid?: boolean;
43 | }
44 |
45 | export type ContainerProps =
46 | PolymorphicComponentPropWithRef;
47 |
48 | export type ContainerComponent = ((
49 | props: ContainerProps
50 | ) => React.ReactElement | null) & { displayName?: string };
51 |
52 | const Container: ContainerComponent = React.forwardRef(
53 | (
54 | {
55 | as,
56 | css,
57 | className = '',
58 | children,
59 | px = 'sm',
60 | responsive = true,
61 | fluid = false,
62 | ...containerProps
63 | }: ContainerProps,
64 | ref?: PolymorphicRef
65 | ) => {
66 | const preClass = 'decaContainer';
67 |
68 | return (
69 |
79 | {children}
80 |
81 | );
82 | }
83 | );
84 |
85 | if (__DEV__) {
86 | Container.displayName = 'DecaUI.Container';
87 | }
88 |
89 | export default Container;
90 |
--------------------------------------------------------------------------------
/src/lib/Container/__snapshots__/Container.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`components/Container matches snapshot 1`] = `
4 |
5 |
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/src/lib/Container/index.ts:
--------------------------------------------------------------------------------
1 | import Container from './Container';
2 | export * from './Container';
3 | export { Container };
4 | export default Container;
5 |
--------------------------------------------------------------------------------
/src/lib/Grid/Grid.stories.tsx:
--------------------------------------------------------------------------------
1 | import Box from '@lib/Box';
2 | import Grid from '@lib/Grid';
3 | import { ComponentMeta, ComponentStory } from '@storybook/react';
4 | import React from 'react';
5 |
6 | import GridRuler from './GridRuler';
7 |
8 | export default {
9 | title: 'Grid',
10 | component: Grid,
11 | argTypes: {
12 | justifyContent: {
13 | description: 'JustifyContent css prop',
14 | table: {
15 | type: {
16 | summary:
17 | 'flex-start | center | flex-end | space-between | space-around | space-evenly',
18 | },
19 | },
20 | control: {
21 | type: 'select',
22 | options: [
23 | 'center',
24 | 'flex-start',
25 | 'flex-end',
26 | 'space-between',
27 | 'space-around',
28 | 'space-evenly',
29 | ],
30 | },
31 | },
32 | alignItems: {
33 | description: 'AlignItems css prop',
34 | table: {
35 | type: {
36 | summary: 'flex-start | center | flex-end',
37 | },
38 | },
39 | control: {
40 | type: 'radio',
41 | options: ['flex-start', 'center', 'flex-end'],
42 | },
43 | },
44 | spacing: {
45 | description: 'How much spacing there should be between columns.',
46 | table: {
47 | type: {
48 | summary: 'none | sm | md | lg',
49 | },
50 | },
51 | control: {
52 | type: 'radio',
53 | options: ['none', 'sm', 'md', 'lg'],
54 | },
55 | },
56 | },
57 | } as ComponentMeta;
58 |
59 | const ExampleBox = ({ children }: { children: React.ReactNode }) => (
60 |
72 | {children}
73 |
74 | );
75 |
76 | const Template: ComponentStory = (args: any) => (
77 |
78 |
89 |
90 | 1
91 |
92 |
93 | 2
94 |
95 |
96 | 3
97 |
98 |
99 | 4
100 |
101 |
102 |
103 | );
104 |
105 | export const Default = Template.bind({});
106 | (Default.args as any) = {
107 | justifyContent: 'center',
108 | alignItems: 'flex-start',
109 | spacing: 'sm',
110 | n: 12,
111 | xs: 12,
112 | sm: 6,
113 | md: 4,
114 | lg: 3,
115 | xl: 2,
116 | };
117 |
118 | export const WithGridRuler = Template.bind({});
119 | WithGridRuler.args = { ...Default.args };
120 |
121 | WithGridRuler.decorators = [
122 | (Story, context) => (
123 |
124 |
125 |
126 |
127 | ),
128 | ];
129 |
--------------------------------------------------------------------------------
/src/lib/Grid/Grid.styles.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@lib/Theme/stitches.config';
2 | import { getStaticColor } from '@lib/Utils';
3 | import { transparentize } from 'polished';
4 |
5 | const justifyContentComposer = () => {
6 | const options = [
7 | 'flex-start',
8 | 'center',
9 | 'flex-end',
10 | 'space-between',
11 | 'space-around',
12 | 'space-evenly',
13 | ];
14 | const variantObj: Array<
15 | Array>
16 | > = [];
17 |
18 | options.map((i) => {
19 | variantObj.push([
20 | i as keyof typeof options,
21 | { justifyContent: i } as Record<'justifyContent', keyof typeof options>,
22 | ]);
23 | });
24 | return Object.fromEntries(variantObj);
25 | };
26 |
27 | const alignItemsComposer = () => {
28 | const options = ['flex-start', 'center', 'flex-end'];
29 | const variantObj: Array<
30 | Array>
31 | > = [];
32 |
33 | options.map((i) => {
34 | variantObj.push([
35 | i as keyof typeof options,
36 | { alignItems: i } as Record<'alignItems', keyof typeof options>,
37 | ]);
38 | });
39 | return Object.fromEntries(variantObj);
40 | };
41 |
42 | export const StyledGridItem = styled('div', {
43 | display: 'block',
44 | boxSizing: 'border-box',
45 | });
46 |
47 | export const StyledGridContainer = styled('div', {
48 | display: 'flex',
49 | flexWrap: 'wrap',
50 | boxSizing: 'border-box',
51 | overflow: 'hidden',
52 | variants: {
53 | justifyContent: justifyContentComposer(),
54 | alignItems: alignItemsComposer(),
55 | spacing: {
56 | none: {
57 | m: '$n',
58 | [`& > ${StyledGridItem}`]: {
59 | p: '$n',
60 | },
61 | },
62 | sm: {
63 | m: '-$1',
64 | [`& > ${StyledGridItem}`]: {
65 | p: '$1',
66 | },
67 | },
68 | md: {
69 | m: '-$3',
70 | [`& > ${StyledGridItem}`]: {
71 | p: '$3',
72 | },
73 | },
74 | lg: {
75 | m: '-$5',
76 | [`& > ${StyledGridItem}`]: {
77 | p: '$5',
78 | },
79 | },
80 | },
81 | },
82 | });
83 |
84 | export const StyledGridRuler = styled('div', {
85 | position: 'absolute',
86 | top: 0,
87 | zIndex: -1,
88 | display: 'grid',
89 | gridTemplateColumns: 'repeat(12, 1fr)',
90 | width: '100%',
91 | height: '100vh',
92 | variants: {
93 | spacing: {
94 | none: {
95 | gap: '$n',
96 | },
97 | sm: {
98 | gap: 'calc($1 * 2)',
99 | },
100 | md: {
101 | gap: 'calc($3 * 2)',
102 | },
103 | lg: {
104 | gap: 'calc($5 * 2)',
105 | },
106 | },
107 | },
108 | });
109 |
110 | export const StyledGridRulerItem = styled('div', {
111 | bg: transparentize(0.75, getStaticColor('gray300')),
112 | border: `${transparentize(0.75, getStaticColor('gray600'))} solid 1px`,
113 | });
114 |
--------------------------------------------------------------------------------
/src/lib/Grid/Grid.test.tsx:
--------------------------------------------------------------------------------
1 | import Grid from '@lib/Grid';
2 | import { render } from '@testing-library/react';
3 | import '@testing-library/jest-dom';
4 | import React from 'react';
5 |
6 | describe('components/Grid', () => {
7 | it('renders content', () => {
8 | const { getByText } = render(
9 |
10 | 1
11 | 2
12 | 3
13 |
14 | );
15 | expect(getByText('1')).toBeInTheDocument();
16 | expect(getByText('2')).toBeInTheDocument();
17 | expect(getByText('3')).toBeInTheDocument();
18 | });
19 | it('matches snapshot', () => {
20 | const { asFragment } = render(
21 |
22 | 1
23 | 2
24 | 3
25 |
26 | );
27 | expect(asFragment()).toMatchSnapshot();
28 | });
29 | it('all sizes render properly on grid container', () => {
30 | const { asFragment } = render(
31 |
32 | 1
33 | 2
34 | 3
35 |
36 | );
37 | expect(asFragment()).toMatchSnapshot();
38 | });
39 | it('all sizes render properly on grid item', () => {
40 | const { asFragment } = render(
41 |
42 |
43 | 1
44 |
45 | 2
46 | 3
47 |
48 | );
49 | expect(asFragment()).toMatchSnapshot();
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/src/lib/Grid/Grid.tsx:
--------------------------------------------------------------------------------
1 | import { CSS } from '@lib/Theme/stitches.config';
2 | import {
3 | PolymorphicRef,
4 | PolymorphicComponentPropWithRef,
5 | __DEV__,
6 | MasterComponent,
7 | } from '@lib/Utils';
8 | import clsx from 'clsx';
9 | import React from 'react';
10 |
11 | import { StyledGridItem } from './Grid.styles';
12 | import GridContainer from './GridContainer';
13 |
14 | export type Cols = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
15 |
16 | /**
17 | * The Grid component acts as a child to the GridContainer component
18 | */
19 | interface Props {
20 | /**
21 | * Override default CSS style.
22 | */
23 | css?: CSS;
24 | /**
25 | * The content of the component.
26 | */
27 | children?: React.ReactNode | undefined;
28 | /**
29 | * ClassName applied to the component.
30 | * @default ''
31 | */
32 | className?: string;
33 | /**
34 | * How many columns should be taken up by item initially
35 | */
36 | n?: Cols;
37 | /**
38 | * How many columns should be taken up by item on xs breakpoint
39 | */
40 | xs?: Cols;
41 | /**
42 | * How many columns should be taken up by item on sm breakpoint
43 | */
44 | sm?: Cols;
45 | /**
46 | * How many columns should be taken up by item on md breakpoint
47 | */
48 | md?: Cols;
49 | /**
50 | * How many columns should be taken up by item on lg breakpoint
51 | */
52 | lg?: Cols;
53 | /**
54 | * How many columns should be taken up by item on xl breakpoint
55 | */
56 | xl?: Cols;
57 | }
58 |
59 | export type GridProps =
60 | PolymorphicComponentPropWithRef;
61 |
62 | export type GridComponent = ((
63 | props: GridProps
64 | ) => React.ReactElement | null) & { displayName?: string };
65 |
66 | const Grid: GridComponent = React.forwardRef(
67 | (
68 | {
69 | as,
70 | css,
71 | className = '',
72 | children,
73 | n,
74 | xs,
75 | sm,
76 | md,
77 | lg,
78 | xl,
79 | ...gridProps
80 | }: GridProps,
81 | ref?: PolymorphicRef
82 | ) => {
83 | const preClass = 'decaGrid';
84 |
85 | const genGridItemCss = (breakpoint?: Cols, bp?: CSS) => {
86 | if (bp) {
87 | return {
88 | flexBasis: `calc((${breakpoint} / 12) * 100%)`,
89 | maxWidth: `calc((${breakpoint} / 12) * 100%)`,
90 | ...bp,
91 | };
92 | }
93 | return {
94 | flexBasis: `calc((${breakpoint} / 12) * 100%)`,
95 | maxWidth: `calc((${breakpoint} / 12) * 100%)`,
96 | };
97 | };
98 |
99 | const {
100 | '@n': cssN,
101 | '@xs': cssXs,
102 | '@sm': cssSm,
103 | '@md': cssMd,
104 | '@lg': cssLg,
105 | '@xl': cssXl,
106 | ...otherCss
107 | } = (css as CSS) || {};
108 |
109 | const getCss = {
110 | flexGrow: 0,
111 | '@n': genGridItemCss(n, cssN),
112 | '@xs': genGridItemCss(xs, cssXs),
113 | '@sm': genGridItemCss(sm, cssSm),
114 | '@md': genGridItemCss(md, cssMd),
115 | '@lg': genGridItemCss(lg, cssLg),
116 | '@xl': genGridItemCss(xl, cssXl),
117 | ...otherCss,
118 | };
119 |
120 | return (
121 |
128 | {children}
129 |
130 | );
131 | }
132 | );
133 |
134 | if (__DEV__) {
135 | Grid.displayName = 'DecaUI.Grid';
136 | }
137 |
138 | export default Grid as MasterComponent<
139 | HTMLDivElement,
140 | GridProps,
141 | { Container: typeof GridContainer }
142 | >;
143 |
--------------------------------------------------------------------------------
/src/lib/Grid/GridContainer.tsx:
--------------------------------------------------------------------------------
1 | import { CSS } from '@lib/Theme/stitches.config';
2 | import {
3 | __DEV__,
4 | PolymorphicRef,
5 | PolymorphicComponentPropWithRef,
6 | } from '@lib/Utils';
7 | import clsx from 'clsx';
8 | import React from 'react';
9 |
10 | import { Cols, GridProps } from './Grid';
11 | import { StyledGridContainer } from './Grid.styles';
12 |
13 | /**
14 | * The GridContainer component serves as a wrapper component to the Grid component
15 | */
16 | interface Props {
17 | /**
18 | * Override default CSS style.
19 | */
20 | css?: CSS;
21 | /**
22 | * The content of the component.
23 | */
24 | children?: React.ReactNode | undefined;
25 | /**
26 | * ClassName applied to the component.
27 | * @default ''
28 | */
29 | className?: string;
30 | /**
31 | * How much spacing there should be between columns.
32 | */
33 | spacing?: 'none' | 'sm' | 'md' | 'lg';
34 | /**
35 | * JustifyContent css prop.
36 | */
37 | justifyContent?:
38 | | 'flex-start'
39 | | 'center'
40 | | 'flex-end'
41 | | 'space-between'
42 | | 'space-around'
43 | | 'space-evenly';
44 | /**
45 | * AlignItems css prop.
46 | */
47 | alignItems?: 'flex-start' | 'center' | 'flex-end';
48 | /**
49 | * How many columns should be taken up by item initially
50 | */
51 | n?: Cols;
52 | /**
53 | * How many columns should be taken up by item on xs breakpoint
54 | */
55 | xs?: Cols;
56 | /**
57 | * How many columns should be taken up by item on sm breakpoint
58 | */
59 | sm?: Cols;
60 | /**
61 | * How many columns should be taken up by item on md breakpoint
62 | */
63 | md?: Cols;
64 | /**
65 | * How many columns should be taken up by item on lg breakpoint
66 | */
67 | lg?: Cols;
68 | /**
69 | * How many columns should be taken up by item on xl breakpoint
70 | */
71 | xl?: Cols;
72 | }
73 |
74 | export type GridContainerProps =
75 | PolymorphicComponentPropWithRef;
76 |
77 | export type GridContainerComponent = ((
78 | props: GridContainerProps
79 | ) => React.ReactElement | null) & { displayName?: string };
80 |
81 | const GridContainer: GridContainerComponent = React.forwardRef(
82 | (
83 | {
84 | as,
85 | css,
86 | className = '',
87 | children,
88 | spacing = 'sm',
89 | justifyContent,
90 | alignItems,
91 | n,
92 | xs,
93 | sm,
94 | md,
95 | lg,
96 | xl,
97 | ...gridContainerProps
98 | }: GridContainerProps,
99 | ref?: PolymorphicRef
100 | ) => {
101 | const preClass = 'decaGridContainer';
102 |
103 | return (
104 |
114 | {React.Children.map(
115 | children as React.ReactElement>,
116 | (child: React.ReactElement>) => {
117 | return React.cloneElement(child, {
118 | n: child.props.n ? child.props.n : n,
119 | xs: child.props.xs ? child.props.xs : xs,
120 | sm: child.props.sm ? child.props.sm : sm,
121 | md: child.props.md ? child.props.md : md,
122 | lg: child.props.lg ? child.props.lg : lg,
123 | xl: child.props.xl ? child.props.xl : xl,
124 | });
125 | }
126 | )}
127 |
128 | );
129 | }
130 | );
131 |
132 | if (__DEV__) {
133 | GridContainer.displayName = 'DecaUI.GridContainer';
134 | }
135 |
136 | export default GridContainer;
137 |
--------------------------------------------------------------------------------
/src/lib/Grid/GridRuler.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { __DEV__ } from '@lib/Utils';
3 | import { StyledGridRuler, StyledGridRulerItem } from './Grid.styles';
4 |
5 | /**
6 | * The GridRuler component is meant to be used as a developer tool to ensure items are lined up correctly on a 12 column grid. For this reason, refs are not forwarded to this component.
7 | */
8 | export interface Props {
9 | /**
10 | * How much spacing there should be between columns.
11 | */
12 | spacing?: 'none' | 'sm' | 'md' | 'lg';
13 | }
14 |
15 | const GridRuler = ({ spacing = 'sm' }: Props) => {
16 | return (
17 |
18 | {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((i) => (
19 |
20 | ))}
21 |
22 | );
23 | };
24 |
25 | if (__DEV__) {
26 | GridRuler.displayName = 'DecaUI.GridRuler';
27 | }
28 |
29 | export default GridRuler;
30 |
--------------------------------------------------------------------------------
/src/lib/Grid/__snapshots__/Grid.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`components/Grid all sizes render properly on grid container 1`] = `
4 |
5 |
8 |
11 | 1
12 |
13 |
16 | 2
17 |
18 |
21 | 3
22 |
23 |
24 |
25 | `;
26 |
27 | exports[`components/Grid all sizes render properly on grid item 1`] = `
28 |
29 |
32 |
35 | 1
36 |
37 |
40 | 2
41 |
42 |
45 | 3
46 |
47 |
48 |
49 | `;
50 |
51 | exports[`components/Grid matches snapshot 1`] = `
52 |
53 |
56 |
59 | 1
60 |
61 |
64 | 2
65 |
66 |
69 | 3
70 |
71 |
72 |
73 | `;
74 |
--------------------------------------------------------------------------------
/src/lib/Grid/index.ts:
--------------------------------------------------------------------------------
1 | import Grid from './Grid';
2 | import GridContainer from './GridContainer';
3 |
4 | Grid.Container = GridContainer;
5 |
6 | export * from './Grid';
7 | export * from './GridContainer';
8 | export { Grid };
9 | export default Grid;
10 |
--------------------------------------------------------------------------------
/src/lib/Input/Input.stories.tsx:
--------------------------------------------------------------------------------
1 | import { DecaUIProvider } from '@lib/Theme';
2 | import { ComponentMeta, ComponentStory } from '@storybook/react';
3 | import React from 'react';
4 |
5 | import Input from './Input';
6 |
7 | export default {
8 | title: 'Input',
9 | component: Input,
10 | } as ComponentMeta;
11 |
12 | const Template: ComponentStory = (args) => ;
13 |
14 | export const Solid = Template.bind({});
15 |
16 | Solid.args = {
17 | label: 'Email Address',
18 | size: 'lg',
19 | helperText: 'Please submit query',
20 | variant: 'solid',
21 | placeholder: 'e.g. johndoe@gmail.com',
22 | focusColor: 'primary',
23 | required: true,
24 | disabled: false,
25 | as: 'input',
26 | maxWidth: false,
27 | initialValue: '',
28 | className: '',
29 | pill: false,
30 | };
31 |
32 | export const Outlined = Template.bind({});
33 |
34 | Outlined.args = {
35 | label: 'Email Address',
36 | size: 'lg',
37 | helperText: 'Please submit query',
38 | variant: 'outlined',
39 | focusColor: 'primary',
40 | placeholder: 'e.g. johndoe@gmail.com',
41 | required: true,
42 | disabled: false,
43 | as: 'input',
44 | maxWidth: false,
45 | initialValue: '',
46 | className: '',
47 | pill: false,
48 | };
49 |
50 | export const WithTheme = Template.bind({});
51 |
52 | WithTheme.args = { ...Outlined.args };
53 | WithTheme.decorators = [
54 | (Story) => (
55 |
65 |
66 |
67 | ),
68 | ];
69 |
70 | export const DarkMode = Template.bind({});
71 |
72 | DarkMode.args = { ...Outlined.args };
73 | DarkMode.parameters = { backgrounds: { default: 'dark' } };
74 |
75 | DarkMode.decorators = [
76 | (Story) => (
77 |
78 |
79 |
80 | ),
81 | ];
82 |
--------------------------------------------------------------------------------
/src/lib/Input/Input.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, fireEvent, act } from '@testing-library/react';
2 | import React from 'react';
3 |
4 | import Input from './Input';
5 |
6 | describe('components/Input', () => {
7 | it('matches snapshot', () => {
8 | const { asFragment } = render(
9 |
14 | );
15 | expect(asFragment()).toMatchSnapshot();
16 | });
17 | it('onFocus event fires', () => {
18 | const mockFn = jest.fn();
19 | const utils = render( );
20 | const input = utils.getByLabelText('Label Text');
21 | act(() => {
22 | input.focus();
23 | });
24 | expect(mockFn.mock.calls.length).toBe(1);
25 | });
26 |
27 | it('renders all colors', () => {
28 | const { asFragment } = render(
29 | <>
30 |
31 |
32 |
33 |
34 |
35 | >
36 | );
37 | expect(asFragment()).toMatchSnapshot();
38 | });
39 | it('renders all sizes', () => {
40 | const { asFragment } = render(
41 | <>
42 |
43 |
44 |
45 | >
46 | );
47 | expect(asFragment()).toMatchSnapshot();
48 | });
49 | it('renders all variants', () => {
50 | const { asFragment } = render(
51 | <>
52 |
53 |
54 | >
55 | );
56 | expect(asFragment()).toMatchSnapshot();
57 | });
58 | it('should ignore events when disabled', () => {
59 | const mockFn = jest.fn();
60 | const utils = render( );
61 | const input = utils.getByLabelText('Label Text');
62 | fireEvent.change(input, { target: { value: 'new-value' } });
63 | expect(mockFn.mock.calls.length).toBe(0);
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/src/lib/Input/index.ts:
--------------------------------------------------------------------------------
1 | import Input from './Input';
2 | export * from './Input';
3 | export { Input };
4 | export default Input;
5 |
--------------------------------------------------------------------------------
/src/lib/Modal/Modal.cy.tsx:
--------------------------------------------------------------------------------
1 | import { Test } from '../Utils';
2 | import _cyp from '../../../cypress';
3 | import React from 'react';
4 | import Modal, {
5 | ModalProps,
6 | ModalHeaderProps,
7 | ModalBodyProps,
8 | ModalFooterProps,
9 | } from '.';
10 | import Button from '../Button';
11 | import Text from '../Text';
12 | import Input from '../Input';
13 | import DecaUIProvider from '../Theme';
14 |
15 | interface ModalComposerProps extends ModalProps {
16 | header?: ModalHeaderProps;
17 | body?: ModalBodyProps;
18 | footer?: ModalFooterProps;
19 | }
20 |
21 | const ModalComposer = (props: ModalComposerProps) => (
22 |
23 |
24 |
25 | Welcome to DecaUI
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Close
35 |
36 | Sign Up
37 |
38 |
39 | );
40 |
41 | const modalSelector = '[data-testid="test.modal"]';
42 |
43 | describe('components/Modal', () => {
44 | describe('base', () => {
45 | it('background-color', () => {
46 | cy.mount( );
47 | cy.get(modalSelector).should(
48 | 'have.css',
49 | 'background-color',
50 | Test.color('white')
51 | );
52 | });
53 | });
54 | describe('state', () => {
55 | it('not exist in DOM when not open', () => {
56 | cy.mount( );
57 | cy.get(modalSelector).should('not.exist');
58 | });
59 | it('exists in DOM when open', () => {
60 | cy.mount( );
61 | cy.get(modalSelector).should('exist');
62 | });
63 | });
64 | describe('autoGap', () => {
65 | it('container', () => {
66 | cy.mount( );
67 | cy.get('.decaModal-flexbox').should('have.css', 'gap', Test.space('4'));
68 | });
69 | it('header', () => {
70 | cy.mount( );
71 | cy.get('.decaModalHeader-root').should(
72 | 'have.css',
73 | 'gap',
74 | Test.space('2')
75 | );
76 | });
77 | it('body', () => {
78 | cy.mount( );
79 | cy.get('.decaModalBody-root').should('have.css', 'gap', Test.space('2'));
80 | });
81 | it('footer', () => {
82 | cy.mount( );
83 | cy.get('.decaModalFooter-root').should(
84 | 'have.css',
85 | 'gap',
86 | Test.space('2')
87 | );
88 | });
89 | });
90 | describe('disabled autoGap', () => {
91 | it('container', () => {
92 | cy.mount( );
93 | cy.get('.decaModal-flexbox').should('have.css', 'gap', Test.space('n'));
94 | });
95 | it('header', () => {
96 | cy.mount( );
97 | cy.get('.decaModalHeader-root').should(
98 | 'have.css',
99 | 'gap',
100 | Test.space('n')
101 | );
102 | });
103 | it('body', () => {
104 | cy.mount( );
105 | cy.get('.decaModalBody-root').should('have.css', 'gap', Test.space('n'));
106 | });
107 | it('footer', () => {
108 | cy.mount( );
109 | cy.get('.decaModalFooter-root').should(
110 | 'have.css',
111 | 'gap',
112 | Test.space('n')
113 | );
114 | });
115 | });
116 |
117 | describe('padding', () => {
118 | it('default padding', () => {
119 | cy.mount( );
120 | cy.get(modalSelector).should('have.css', 'padding', Test.space('3'));
121 | });
122 | it('noPadding', () => {
123 | cy.mount( );
124 | cy.get(modalSelector).should('have.css', 'padding', Test.space('n'));
125 | });
126 | });
127 | });
128 |
--------------------------------------------------------------------------------
/src/lib/Modal/Modal.stories.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@lib/Button';
2 | import Input from '@lib/Input';
3 | import Modal from '@lib/Modal';
4 | import Text from '@lib/Text';
5 | import { DecaUIProvider } from '@lib/Theme';
6 | import { ComponentMeta, ComponentStory } from '@storybook/react';
7 | import React, { useState } from 'react';
8 |
9 | export default {
10 | title: 'Modal',
11 | component: Modal,
12 | } as ComponentMeta;
13 |
14 | const Template: ComponentStory = (args) => {
15 | const [open, setOpen] = useState(false);
16 |
17 | return (
18 | <>
19 | setOpen(true)}>Open Modal
20 |
21 |
22 |
23 | Welcome to DecaUI
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | setOpen(false)}>
32 | Close
33 |
34 | Sign Up
35 |
36 |
37 | >
38 | );
39 | };
40 |
41 | export const Default = Template.bind({});
42 | Default.args = {
43 | noPadding: false,
44 | autoGap: true,
45 | closeButton: true,
46 | };
47 |
48 | export const WithTheme = Template.bind({});
49 | WithTheme.args = { ...Default.args };
50 | WithTheme.decorators = [
51 | (Story) => (
52 |
62 |
63 |
64 | ),
65 | ];
66 |
67 | export const DarkMode = Template.bind({});
68 |
69 | DarkMode.args = { ...Default.args };
70 | DarkMode.parameters = { backgrounds: { default: 'dark' } };
71 |
72 | DarkMode.decorators = [
73 | (Story) => (
74 |
75 |
76 |
77 | ),
78 | ];
79 |
--------------------------------------------------------------------------------
/src/lib/Modal/Modal.styles.tsx:
--------------------------------------------------------------------------------
1 | import { styled, theme } from '@lib/Theme/stitches.config';
2 | import { animated } from '@react-spring/web';
3 | import { transparentize } from 'polished';
4 |
5 | export const StyledModalOverlay = styled(animated.div, {
6 | position: 'fixed',
7 | top: 0,
8 | left: 0,
9 | right: 0,
10 | bottom: 0,
11 | bg: transparentize(0.4, theme.colors.black.value),
12 | zIndex: '$10',
13 | });
14 |
15 | export const StyledModal = styled(animated.div, {
16 | position: 'fixed',
17 | fontFamily: '$normal',
18 | boxShadow: '$default',
19 | br: '$sm',
20 | zIndex: '$max',
21 | color: '$text',
22 | variants: {
23 | noPadding: {
24 | true: {
25 | p: '$n',
26 | },
27 | false: {
28 | p: '$3',
29 | },
30 | },
31 | isDark: {
32 | true: {
33 | bg: '$popperDarkBg',
34 | },
35 | false: {
36 | bg: '$popperLightBg',
37 | },
38 | },
39 | },
40 | });
41 |
42 | export const StyledModalFlexbox = styled('div', {
43 | display: 'flex',
44 | justifyContent: 'center',
45 | flexDirection: 'column',
46 | variants: {
47 | autoGap: {
48 | true: {
49 | gap: '$4',
50 | },
51 | false: {
52 | gap: '$n',
53 | },
54 | },
55 | },
56 | });
57 |
58 | export const StyledModalHeader = styled('div', {
59 | color: '$text',
60 | display: 'flex',
61 | justifyContent: 'center',
62 | flexDirection: 'column',
63 | variants: {
64 | autoGap: {
65 | true: {
66 | gap: '$2',
67 | },
68 | false: {
69 | gap: '$n',
70 | },
71 | },
72 | },
73 | });
74 |
75 | export const StyledModalBody = styled('div', {
76 | color: '$text',
77 | display: 'flex',
78 | justifyContent: 'center',
79 | flexDirection: 'column',
80 | variants: {
81 | autoGap: {
82 | true: {
83 | gap: '$2',
84 | },
85 | false: {
86 | gap: '$n',
87 | },
88 | },
89 | },
90 | });
91 |
92 | export const StyledModalFooter = styled('div', {
93 | color: '$text',
94 | display: 'flex',
95 | justifyContent: 'flex-end',
96 | flexDirection: 'row',
97 | variants: {
98 | autoGap: {
99 | true: {
100 | gap: '$2',
101 | },
102 | false: {
103 | gap: '$n',
104 | },
105 | },
106 | },
107 | });
108 |
--------------------------------------------------------------------------------
/src/lib/Modal/Modal.test.tsx:
--------------------------------------------------------------------------------
1 | import Modal from '@lib/Modal';
2 | import { render, screen } from '@testing-library/react';
3 | import React from 'react';
4 |
5 | describe('components/Modal', () => {
6 | global.ResizeObserver = require('resize-observer-polyfill');
7 | it('matches snapshot', () => {
8 | const { asFragment } = render(Hello World );
9 | expect(asFragment()).toMatchSnapshot();
10 | });
11 | it('renders text when modal is open', () => {
12 | render(content );
13 | expect(screen.queryByText('content')).not.toBe(null);
14 | });
15 | it('does not render text when modal is closed', () => {
16 | render(content );
17 | expect(screen.queryByText('content')).toBe(null);
18 | });
19 | it('modal components all render correctly', () => {
20 | const { asFragment } = render(
21 |
22 | Header component
23 | Header component
24 | Header component
25 |
26 | );
27 | expect(asFragment()).toMatchSnapshot();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/src/lib/Modal/ModalBody.tsx:
--------------------------------------------------------------------------------
1 | import { CSS } from '@lib/Theme';
2 | import {
3 | __DEV__,
4 | PolymorphicRef,
5 | PolymorphicComponentPropWithRef,
6 | } from '@lib/Utils';
7 | import clsx from 'clsx';
8 | import React, { useContext } from 'react';
9 |
10 | import { ModalContext, IModalContext } from './Modal';
11 | import { StyledModalBody } from './Modal.styles';
12 |
13 | /**
14 | * ModalBody contains the main content of a modal component
15 | */
16 | interface Props {
17 | /**
18 | * The content of the component.
19 | */
20 | children?: React.ReactNode | undefined;
21 | /**
22 | * ClassName applied to the component.
23 | * @default ''
24 | */
25 | className?: string;
26 | /**
27 | * Override default CSS style.
28 | */
29 | css?: CSS;
30 | /**
31 | * Have gap between all elements.
32 | */
33 | autoGap?: boolean;
34 | }
35 |
36 | export type ModalBodyProps =
37 | PolymorphicComponentPropWithRef;
38 |
39 | export type ModalBodyComponent = ((
40 | props: ModalBodyProps
41 | ) => React.ReactElement | null) & { displayName?: string };
42 |
43 | const ModalBody: ModalBodyComponent = React.forwardRef(
44 | (
45 | { children, className = '', css, as, autoGap, ...props }: ModalBodyProps,
46 | ref?: PolymorphicRef
47 | ) => {
48 | const context = useContext(ModalContext) as IModalContext;
49 |
50 | const preClass = 'decaModalBody';
51 |
52 | return (
53 |
61 | {children}
62 |
63 | );
64 | }
65 | );
66 |
67 | if (__DEV__) {
68 | ModalBody.displayName = 'DecaUI.ModalBody';
69 | }
70 |
71 | export default ModalBody;
72 |
--------------------------------------------------------------------------------
/src/lib/Modal/ModalFooter.tsx:
--------------------------------------------------------------------------------
1 | import { CSS } from '@lib/Theme/stitches.config';
2 | import {
3 | __DEV__,
4 | PolymorphicRef,
5 | PolymorphicComponentPropWithRef,
6 | } from '@lib/Utils';
7 | import clsx from 'clsx';
8 | import React, { useContext } from 'react';
9 |
10 | import { ModalContext, IModalContext } from './Modal';
11 | import { StyledModalFooter } from './Modal.styles';
12 |
13 | /**
14 | * ModalFooter allows users to place content on the bottom of their modal component
15 | */
16 | interface Props {
17 | /**
18 | * The content of the component.
19 | */
20 | children?: React.ReactNode | undefined;
21 | /**
22 | * ClassName applied to the component.
23 | * @default ''
24 | */
25 | className?: string;
26 | /**
27 | * Override default CSS style.
28 | */
29 | css?: CSS;
30 | /**
31 | * Have gap between all elements.
32 | */
33 | autoGap?: boolean;
34 | }
35 |
36 | export type ModalFooterProps =
37 | PolymorphicComponentPropWithRef;
38 |
39 | export type ModalFooterComponent = ((
40 | props: ModalFooterProps
41 | ) => React.ReactElement | null) & { displayName?: string };
42 |
43 | const ModalFooter: ModalFooterComponent = React.forwardRef(
44 | (
45 | {
46 | children,
47 | className = '',
48 | css,
49 | as,
50 | autoGap,
51 | ...props
52 | }: ModalFooterProps,
53 | ref?: PolymorphicRef
54 | ) => {
55 | const context = useContext(ModalContext) as IModalContext;
56 |
57 | const preClass = 'decaModalFooter';
58 |
59 | return (
60 |
68 | {children}
69 |
70 | );
71 | }
72 | );
73 |
74 | if (__DEV__) {
75 | ModalFooter.displayName = 'DecaUI.ModalFooter';
76 | }
77 |
78 | export default ModalFooter;
79 |
--------------------------------------------------------------------------------
/src/lib/Modal/ModalHeader.tsx:
--------------------------------------------------------------------------------
1 | import { CSS } from '@lib/Theme/stitches.config';
2 | import {
3 | __DEV__,
4 | PolymorphicRef,
5 | PolymorphicComponentPropWithRef,
6 | } from '@lib/Utils';
7 | import clsx from 'clsx';
8 | import React, { useContext } from 'react';
9 |
10 | import { ModalContext, IModalContext } from './Modal';
11 | import { StyledModalHeader } from './Modal.styles';
12 |
13 | /**
14 | * ModalHeader allows users to place a header on their modal component
15 | */
16 | interface Props {
17 | /**
18 | * The content of the component.
19 | */
20 | children?: React.ReactNode | undefined;
21 | /**
22 | * ClassName applied to the component.
23 | * @default ''
24 | */
25 | className?: string;
26 | /**
27 | * Override default CSS style.
28 | */
29 | css?: CSS;
30 | /**
31 | * Have gap between all elements.
32 | */
33 | autoGap?: boolean;
34 | }
35 |
36 | export type ModalHeaderProps =
37 | PolymorphicComponentPropWithRef;
38 |
39 | export type ModalHeaderComponent = ((
40 | props: ModalHeaderProps
41 | ) => React.ReactElement | null) & { displayName?: string };
42 |
43 | const ModalHeader: ModalHeaderComponent = React.forwardRef(
44 | (
45 | {
46 | children,
47 | className = '',
48 | css,
49 | as,
50 | autoGap,
51 | ...props
52 | }: ModalHeaderProps,
53 | ref?: PolymorphicRef
54 | ) => {
55 | const context = useContext(ModalContext) as IModalContext;
56 |
57 | const preClass = 'decaModalHeader';
58 |
59 | return (
60 |
68 | {children}
69 |
70 | );
71 | }
72 | );
73 |
74 | if (__DEV__) {
75 | ModalHeader.displayName = 'DecaUI.ModalHeader';
76 | }
77 |
78 | export default ModalHeader;
79 |
--------------------------------------------------------------------------------
/src/lib/Modal/__snapshots__/Modal.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`components/Modal matches snapshot 1`] = ` `;
4 |
5 | exports[`components/Modal modal components all render correctly 1`] = ` `;
6 |
--------------------------------------------------------------------------------
/src/lib/Modal/index.ts:
--------------------------------------------------------------------------------
1 | import Modal from './Modal';
2 | import ModalBody from './ModalBody';
3 | import ModalFooter from './ModalFooter';
4 | import ModalHeader from './ModalHeader';
5 |
6 | Modal.Header = ModalHeader;
7 | Modal.Body = ModalBody;
8 | Modal.Footer = ModalFooter;
9 |
10 | export * from './Modal';
11 | export * from './ModalHeader';
12 | export * from './ModalBody';
13 | export * from './ModalFooter';
14 | export { Modal };
15 | export default Modal;
16 |
--------------------------------------------------------------------------------
/src/lib/Popover/Popover.cy.tsx:
--------------------------------------------------------------------------------
1 | import { Test } from '../Utils';
2 | import _cyp from '../../../cypress';
3 | import React from 'react';
4 | import Popover, { PopoverProps } from '.';
5 | import Button from '../Button';
6 |
7 | const PopoverComposer = (props: PopoverProps) => (
8 |
9 |
10 | Open Popover
11 |
12 | This is the content of the popover.
13 |
14 | );
15 |
16 | describe('components/Popover', () => {
17 | describe('base', () => {
18 | it('background-color', () => {
19 | cy.mount( );
20 | cy.get('button').click();
21 | cy.get('.decaPopover-root').should(
22 | 'have.css',
23 | 'background-color',
24 | Test.color('popperLightBg')
25 | );
26 | });
27 | it('color', () => {
28 | cy.mount( );
29 | cy.get('button').click();
30 | cy.get('.decaPopover-root').should(
31 | 'have.css',
32 | 'color',
33 | Test.color('black')
34 | );
35 | });
36 | });
37 | describe('state', () => {
38 | it('not exist in DOM when not open', () => {
39 | cy.mount( );
40 | cy.get('.decaPopover-root').should('not.exist');
41 | });
42 | it('exists in DOM when open (button clicked)', () => {
43 | cy.mount( );
44 | cy.get('button').click();
45 | cy.get('.decaPopover-root').should('exist');
46 | });
47 | });
48 | describe('dark mode', () => {
49 | it('background-color', () => {
50 | cy.darkMount( );
51 | cy.get('button').click();
52 | cy.get('.decaPopover-root').should(
53 | 'have.css',
54 | 'background-color',
55 | Test.color('popperDarkBg')
56 | );
57 | });
58 | it('color', () => {
59 | cy.darkMount( );
60 | cy.get('button').click();
61 | cy.get('.decaPopover-root').should(
62 | 'have.css',
63 | 'color',
64 | Test.color('white')
65 | );
66 | });
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/src/lib/Popover/Popover.stories.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@lib/Button';
2 | import Popover from '@lib/Popover';
3 | import { DecaUIProvider } from '@lib/Theme';
4 | import { ComponentMeta, ComponentStory } from '@storybook/react';
5 | import React from 'react';
6 |
7 | export default {
8 | title: 'Popover',
9 | component: Popover,
10 | } as ComponentMeta;
11 |
12 | const Template: ComponentStory = (args) => (
13 |
14 |
15 | Open Popover
16 |
17 | This is the content of the popover.
18 |
19 | );
20 |
21 | export const Default = Template.bind({});
22 | Default.args = {
23 | placement: 'bottom',
24 | action: 'click',
25 | };
26 |
27 | export const WithTheme = Template.bind({});
28 |
29 | WithTheme.args = { ...Default.args };
30 | WithTheme.decorators = [
31 | (Story) => (
32 |
42 |
43 |
44 | ),
45 | ];
46 |
47 | export const DarkMode = Template.bind({});
48 |
49 | DarkMode.args = { ...Default.args };
50 | DarkMode.parameters = { backgrounds: { default: 'dark' } };
51 |
52 | DarkMode.decorators = [
53 | (Story) => (
54 |
55 |
56 |
57 | ),
58 | ];
59 |
--------------------------------------------------------------------------------
/src/lib/Popover/Popover.styles.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@lib/Theme/stitches.config';
2 | import { animated } from '@react-spring/web';
3 |
4 | export const StyledPopover = styled(animated.div, {
5 | fontFamily: '$normal',
6 | p: '$3',
7 | boxShadow: '$default',
8 | br: '$sm',
9 | color: '$text',
10 | zIndex: '$5',
11 | variants: {
12 | isDark: {
13 | true: {
14 | bg: '$popperDarkBg',
15 | },
16 | false: {
17 | bg: '$popperLightBg',
18 | },
19 | },
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/src/lib/Popover/Popover.test.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@lib/Button';
2 | import Popover from '@lib/Popover';
3 | import { render, fireEvent, act, screen } from '@testing-library/react';
4 | import React from 'react';
5 |
6 | describe('components/Popover', () => {
7 | global.ResizeObserver = require('resize-observer-polyfill');
8 | it('matches snapshot', () => {
9 | const { asFragment } = render(
10 |
11 |
12 | Open Popover
13 |
14 | content
15 |
16 | );
17 | expect(asFragment()).toMatchSnapshot();
18 | });
19 | it('click action', () => {
20 | const utils = render(
21 |
22 |
23 | Open Popover
24 |
25 | content
26 |
27 | );
28 | expect(screen.queryByText('content')).toBe(null);
29 | const popoverTrigger = utils.getByText('Open Popover');
30 | act(() => {
31 | popoverTrigger.click();
32 | });
33 | expect(screen.queryByText('content')).not.toBe(null);
34 | });
35 | it('hover action', async () => {
36 | const utils = render(
37 |
38 |
39 | Open Popover
40 |
41 | content
42 |
43 | );
44 | expect(screen.queryByText('content')).toBe(null);
45 | const popoverTrigger = utils.getByText('Open Popover');
46 | act(() => {
47 | fireEvent.mouseEnter(popoverTrigger);
48 | });
49 | expect(screen.queryByText('content')).not.toBe(null);
50 | act(() => {
51 | fireEvent.mouseLeave(popoverTrigger);
52 | });
53 | await act(async () => new Promise((r) => setTimeout(r, 1000)));
54 | expect(screen.queryByText('content')).toBe(null);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/src/lib/Popover/Popover.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | useFloating,
3 | offset as floatingOffset,
4 | flip,
5 | shift,
6 | autoUpdate,
7 | UseFloatingReturn,
8 | Placement,
9 | } from '@floating-ui/react-dom';
10 | import {
11 | MasterComponent,
12 | PolymorphicRef,
13 | PolymorphicComponentPropWithRef,
14 | __DEV__,
15 | } from '@lib/Utils';
16 | import React, {
17 | useState,
18 | useEffect,
19 | useMemo,
20 | SetStateAction,
21 | Dispatch,
22 | } from 'react';
23 | import PopoverTrigger from './PopoverTrigger';
24 | import PopoverContent from './PopoverContent';
25 |
26 | /**
27 | * A Popover can be used to display some content on top of another.
28 | */
29 | export interface Props {
30 | /**
31 | * The content of the component. It is usually the `Popover.Trigger`,
32 | * and `Popover.Content`
33 | */
34 | children?: React.ReactNode[];
35 | /*
36 | * If true, the component is shown.
37 | */
38 | open?: boolean;
39 | /**
40 | * State dispatcher function (setter in useState)
41 | */
42 | setOpen?: Dispatch>;
43 | /**
44 | * Placement of the popover component
45 | * @default bottom
46 | */
47 | placement?: Placement;
48 | /**
49 | * Determines what action needs to take place in order for popover to appear
50 | * @default click
51 | */
52 | action?: 'click' | 'hover';
53 | /**
54 | * How far away PopoverContent should be away from PopoverTrigger when opened
55 | * @default 10
56 | */
57 | offset?: number;
58 | }
59 |
60 | export interface IPopoverContext extends UseFloatingReturn {
61 | triggerRef?: React.Ref;
62 | open?: boolean;
63 | setOpen?: Dispatch>;
64 | mainComponentRef: PolymorphicRef;
65 | action: 'click' | 'hover';
66 | }
67 |
68 | export const PopoverContext = React.createContext(null);
69 |
70 | export type PopoverProps =
71 | PolymorphicComponentPropWithRef;
72 |
73 | export type PopoverComponent = ((
74 | props: PopoverProps
75 | ) => React.ReactElement | null) & { displayName?: string };
76 |
77 | const Popover: PopoverComponent = React.forwardRef(
78 | (
79 | {
80 | children,
81 | open,
82 | setOpen,
83 | placement = 'bottom',
84 | action = 'click',
85 | offset = 10,
86 | }: PopoverProps,
87 | ref?: PolymorphicRef
88 | ) => {
89 | const floatingProps = useFloating({
90 | placement: placement,
91 | whileElementsMounted: autoUpdate,
92 | strategy: 'absolute',
93 | middleware: [floatingOffset(offset), flip(), shift()],
94 | });
95 |
96 | const [selfOpen, setSelfOpen] = useState(false);
97 |
98 | const [scrollPos, setScrollPos] = useState(0);
99 |
100 | const isControlledComponent = useMemo(() => open !== undefined, [open]);
101 |
102 | const isScrolling = () => {
103 | if (window.scrollY !== scrollPos) {
104 | isControlledComponent ? setOpen && setOpen(false) : setSelfOpen(false);
105 | setScrollPos(window.scrollY);
106 | }
107 | };
108 |
109 | const handleEsc = (e: KeyboardEvent) => {
110 | if (e.key === 'Escape') {
111 | isControlledComponent ? setOpen && setOpen(false) : setSelfOpen(false);
112 | }
113 | };
114 |
115 | useEffect(() => {
116 | window.addEventListener('scroll', isScrolling);
117 | window.addEventListener('keydown', handleEsc);
118 | return () => {
119 | window.removeEventListener('scroll', isScrolling);
120 | window.removeEventListener('keydown', handleEsc);
121 | };
122 | }, []);
123 |
124 | const [trigger, content] = React.Children.toArray(children);
125 |
126 | const triggerRef = React.useRef();
127 |
128 | return (
129 |
139 | {trigger}
140 | {content}
141 |
142 | );
143 | }
144 | );
145 |
146 | if (__DEV__) {
147 | Popover.displayName = 'DecaUI.Popover';
148 | }
149 |
150 | export default Popover as MasterComponent<
151 | HTMLDivElement,
152 | PopoverProps,
153 | {
154 | Trigger: typeof PopoverTrigger;
155 | Content: typeof PopoverContent;
156 | }
157 | >;
158 |
--------------------------------------------------------------------------------
/src/lib/Popover/PopoverContent.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeContext } from '@lib/Theme';
2 | import { CSS } from '@lib/Theme/stitches.config';
3 | import {
4 | PolymorphicRef,
5 | PolymorphicComponentPropWithRef,
6 | mergeRefs,
7 | useClickOutside,
8 | __DEV__,
9 | } from '@lib/Utils';
10 | import { animated, useTransition } from '@react-spring/web';
11 | import clsx from 'clsx';
12 | import React, { useState, useEffect, useContext } from 'react';
13 | import ReactDOM from 'react-dom';
14 |
15 | import { PopoverContext, IPopoverContext } from './Popover';
16 | import { StyledPopover } from './Popover.styles';
17 |
18 | /**
19 | * PopoverContent contains the content shown when the trigger is executed
20 | */
21 | interface Props {
22 | /**
23 | * The content of the component.
24 | */
25 | children?: React.ReactNode | undefined;
26 | /**
27 | * Override default CSS style.
28 | */
29 | css?: CSS;
30 | /**
31 | * ClassName applied to the component.
32 | * @default ''
33 | */
34 | className?: string;
35 | }
36 |
37 | export type PopoverContentProps =
38 | PolymorphicComponentPropWithRef;
39 |
40 | export type PopoverContentComponent = ((
41 | props: PopoverContentProps
42 | ) => React.ReactElement | null) & { displayName?: string };
43 |
44 | const PopoverContent: PopoverContentComponent = React.forwardRef(
45 | (
46 | { children, css, className = '', as, ...props }: PopoverContentProps,
47 | ref?: PolymorphicRef
48 | ) => {
49 | const context = useContext(PopoverContext) as IPopoverContext;
50 |
51 | const clickOutsideRef = useClickOutside(() => {
52 | context.setOpen && context.setOpen(false);
53 | }, [context.triggerRef]);
54 |
55 | const transition = useTransition(context.open, {
56 | from: {
57 | scale: 0.75,
58 | opacity: 0,
59 | },
60 | enter: {
61 | scale: 1,
62 | opacity: 1,
63 | },
64 | leave: {
65 | scale: 0.75,
66 | opacity: 0,
67 | },
68 | config: {
69 | tension: 300,
70 | friction: 19,
71 | },
72 | });
73 |
74 | const preClass = 'decaPopover';
75 |
76 | const { dark } = React.useContext(ThemeContext);
77 |
78 | const [DOM, setDOM] = useState(false);
79 |
80 | useEffect(() => {
81 | setDOM(true);
82 | }, []);
83 |
84 | if (DOM) {
85 | return ReactDOM.createPortal(
86 | transition(
87 | (style, item) =>
88 | item && (
89 |
108 | {children}
109 |
110 | )
111 | ),
112 | document.getElementById('decaUI-provider')
113 | ? (document.getElementById('decaUI-provider') as Element)
114 | : (document.querySelector('body') as Element)
115 | );
116 | }
117 | return <>>;
118 | }
119 | );
120 |
121 | if (__DEV__) {
122 | PopoverContent.displayName = 'DecaUI.PopoverContent';
123 | }
124 |
125 | export default PopoverContent;
126 |
--------------------------------------------------------------------------------
/src/lib/Popover/PopoverTrigger.tsx:
--------------------------------------------------------------------------------
1 | import { mergeRefs, __DEV__ } from '@lib/Utils';
2 | import React, { useContext } from 'react';
3 |
4 | import { PopoverContext, IPopoverContext } from './Popover';
5 |
6 | /**
7 | * PopoverTrigger opens the popover's content. It must be an interactive element
8 | * such as `button` or `a`.
9 | */
10 | export interface Props {
11 | /**
12 | * The content of the component.
13 | */
14 | children?: React.ReactNode | undefined;
15 | }
16 |
17 | const PopoverTrigger = ({ children }: Props) => {
18 | const context = useContext(PopoverContext) as IPopoverContext;
19 |
20 | // enforce single child
21 | const child: any = React.Children.only(children);
22 |
23 | if (context.action === 'click') {
24 | const extendedOnClick = () => {
25 | context.setOpen && context.setOpen((prevState) => !prevState);
26 | child.props.onClick && child.props.onClick();
27 | };
28 |
29 | return React.cloneElement(child, {
30 | ...child.props,
31 | onClick: extendedOnClick,
32 | ref: mergeRefs(context.reference, child.ref, context.triggerRef),
33 | });
34 | } else {
35 | const extendedOnMouseEnter = () => {
36 | context.setOpen && context.setOpen(true);
37 | child.props.onMouseEnter && child.props.onMouseEnter();
38 | };
39 |
40 | const extendedOnMouseLeave = () => {
41 | context.setOpen && context.setOpen(false);
42 | child.props.onMouseLeave && child.props.onMouseLeave();
43 | };
44 |
45 | return React.cloneElement(child, {
46 | ...child.props,
47 | onMouseEnter: extendedOnMouseEnter,
48 | onMouseLeave: extendedOnMouseLeave,
49 | ref: mergeRefs(context.reference, child.ref, context.triggerRef),
50 | });
51 | }
52 | };
53 |
54 | if (__DEV__) {
55 | PopoverTrigger.displayName = 'DecaUI.PopoverTrigger';
56 | }
57 |
58 | export default PopoverTrigger;
59 |
--------------------------------------------------------------------------------
/src/lib/Popover/__snapshots__/Popover.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`components/Popover matches snapshot 1`] = `
4 |
5 |
9 | Open Popover
10 |
11 |
12 | `;
13 |
--------------------------------------------------------------------------------
/src/lib/Popover/index.ts:
--------------------------------------------------------------------------------
1 | import Popover from './Popover';
2 | import PopoverContent from './PopoverContent';
3 | import PopoverTrigger from './PopoverTrigger';
4 |
5 | Popover.Trigger = PopoverTrigger;
6 | Popover.Content = PopoverContent;
7 |
8 | export * from './Popover';
9 | export { Popover };
10 | export default Popover;
11 |
--------------------------------------------------------------------------------
/src/lib/Radio/Radio.cy.tsx:
--------------------------------------------------------------------------------
1 | import { standardColors } from '../Theme';
2 | import { Test } from '../Utils';
3 | import _cyp from '../../../cypress';
4 | import React from 'react';
5 | import Radio from './Radio';
6 |
7 | describe('components/Radio', () => {
8 | describe('before click', () => {
9 | it('border-color', () => {
10 | cy.mount( );
11 |
12 | cy.get('label')
13 | .before('border-color')
14 | .should('eq', Test.color('gray600'));
15 | });
16 | it('disabled', () => {
17 | cy.mount( );
18 |
19 | cy.get('label').should('have.css', 'color', Test.color('gray500'));
20 |
21 | cy.get('label')
22 | .before('border-color')
23 | .should('eq', Test.color('gray400'));
24 | });
25 | });
26 | describe('colors', () => {
27 | it('label color', () => {
28 | cy.mount( );
29 | cy.get('label').should('have.css', 'color', Test.color('black'));
30 | });
31 | standardColors.map((color) => {
32 | describe(color, () => {
33 | it('background-color', () => {
34 | cy.mount( );
35 |
36 | cy.get('label').click();
37 |
38 | // wait for css transition to finish
39 | cy.wait(250);
40 | cy.get('.decaRadio-circle').should(
41 | 'have.css',
42 | 'background-color',
43 | Test.color(color)
44 | );
45 | });
46 | it('border-color', () => {
47 | cy.mount( );
48 |
49 | cy.get('label').click();
50 |
51 | // wait for css transition to finish
52 | cy.wait(250);
53 | cy.get('label')
54 | .before('border-color')
55 | .should('eq', Test.color(color));
56 | });
57 | it('disabled', () => {
58 | cy.mount(
59 |
60 | );
61 |
62 | cy.get('label').before('opacity').should('eq', '0.6');
63 | cy.get('.decaRadio-circle').should('have.css', 'opacity', '0.5');
64 | });
65 | });
66 | });
67 | });
68 | describe('sizes', () => {
69 | it('sm', () => {
70 | cy.mount( );
71 | cy.get('label').before('width').should('eq', Test.size('2'));
72 | cy.get('label').before('height').should('eq', Test.size('2'));
73 | cy.get('label').before('margin-right').should('eq', Test.space('1'));
74 | cy.get('label').should('have.css', 'font-size', Test.fontSize('caption'));
75 | cy.get('.decaRadio-circle').should('have.css', 'width', Test.size('1'));
76 | });
77 | it('md', () => {
78 | cy.mount( );
79 | cy.get('label').before('width').should('eq', Test.size('3'));
80 | cy.get('label').before('height').should('eq', Test.size('3'));
81 | cy.get('label').before('margin-right').should('eq', Test.space('2'));
82 | cy.get('label').should('have.css', 'font-size', Test.fontSize('bodySm'));
83 | cy.get('.decaRadio-circle').should('have.css', 'width', Test.size('2'));
84 | });
85 | it('lg', () => {
86 | cy.mount( );
87 | cy.get('label').before('width').should('eq', Test.size('4'));
88 | cy.get('label').before('height').should('eq', Test.size('4'));
89 | cy.get('label').before('margin-right').should('eq', Test.space('2'));
90 | cy.get('label').should('have.css', 'font-size', Test.fontSize('body'));
91 | cy.get('.decaRadio-circle').should('have.css', 'width', Test.size('3'));
92 | });
93 | });
94 |
95 | describe('no label', () => {
96 | describe('should have no margin', () => {
97 | it('sm', () => {
98 | cy.mount( );
99 | cy.get('label').before('margin-right').should('eq', '0px');
100 | });
101 | it('md', () => {
102 | cy.mount( );
103 | cy.get('label').before('margin-right').should('eq', '0px');
104 | });
105 | it('sm', () => {
106 | cy.mount( );
107 | cy.get('label').before('margin-right').should('eq', '0px');
108 | });
109 | });
110 | });
111 |
112 | describe('dark mode', () => {
113 | it('label color', () => {
114 | cy.darkMount( );
115 | cy.get('label').should('have.css', 'color', Test.color('white'));
116 | });
117 |
118 | it('disabled state', () => {
119 | cy.darkMount( );
120 | cy.get('label').before('opacity').should('eq', '0.35');
121 | cy.get('.decaRadio-circle').should('have.css', 'opacity', '0.35');
122 | });
123 | });
124 | });
125 |
--------------------------------------------------------------------------------
/src/lib/Radio/Radio.stories.tsx:
--------------------------------------------------------------------------------
1 | import { DecaUIProvider } from '@lib/Theme';
2 | import { ComponentMeta, ComponentStory } from '@storybook/react';
3 | import React from 'react';
4 |
5 | import Radio from './Radio';
6 |
7 | export default {
8 | title: 'Radio',
9 | component: Radio,
10 | } as ComponentMeta;
11 |
12 | const Template: ComponentStory = (args) => ;
13 |
14 | export const Default = Template.bind({});
15 | Default.args = {
16 | label: 'Label',
17 | size: 'md',
18 | color: 'primary',
19 | disabled: false,
20 | css: {},
21 | className: '',
22 | };
23 |
24 | export const NoLabel = Template.bind({});
25 | NoLabel.args = {
26 | ...Default.args,
27 | label: '',
28 | };
29 |
30 | export const WithTheme = Template.bind({});
31 |
32 | WithTheme.args = { ...Default.args };
33 | WithTheme.decorators = [
34 | (Story) => (
35 |
42 |
43 |
44 | ),
45 | ];
46 |
47 | export const DarkMode = Template.bind({});
48 |
49 | DarkMode.args = { ...Default.args };
50 | DarkMode.parameters = { backgrounds: { default: 'dark' } };
51 |
52 | DarkMode.decorators = [
53 | (Story) => (
54 |
55 |
56 |
57 | ),
58 | ];
59 |
--------------------------------------------------------------------------------
/src/lib/Radio/Radio.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import React from 'react';
3 |
4 | import Radio from './Radio';
5 |
6 | describe('components/Radio', () => {
7 | it('matches snapshot', () => {
8 | const { asFragment } = render( );
9 | expect(asFragment()).toMatchSnapshot();
10 | });
11 | it('renders all colors', () => {
12 | const { asFragment } = render(
13 | <>
14 |
15 |
16 |
17 |
18 |
19 | >
20 | );
21 | expect(asFragment()).toMatchSnapshot();
22 | });
23 | it('renders all sizes', () => {
24 | const { asFragment } = render(
25 | <>
26 |
27 |
28 |
29 | >
30 | );
31 | expect(asFragment()).toMatchSnapshot();
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/src/lib/Radio/RadioGroup/RadioGroup.stories.tsx:
--------------------------------------------------------------------------------
1 | import Radio from '@lib/Radio';
2 | import { DecaUIProvider } from '@lib/Theme';
3 | import { ComponentMeta, ComponentStory } from '@storybook/react';
4 | import React from 'react';
5 |
6 | export default {
7 | title: 'RadioGroup',
8 | component: Radio.Group,
9 | } as ComponentMeta;
10 |
11 | const Template: ComponentStory = (args) => (
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 | export const Default = Template.bind({});
21 | Default.args = {
22 | defaultValue: 'A',
23 | name: 'FormGroup-Radio',
24 | disabled: false,
25 | className: '',
26 | color: 'primary',
27 | };
28 |
29 | export const SingleDisabled = () => (
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 |
38 | export const WithTheme = Template.bind({});
39 |
40 | WithTheme.args = { ...Default.args };
41 | WithTheme.decorators = [
42 | (Story) => (
43 |
50 |
51 |
52 | ),
53 | ];
54 |
55 | export const DarkMode = Template.bind({});
56 |
57 | DarkMode.args = { ...Default.args };
58 | DarkMode.parameters = { backgrounds: { default: 'dark' } };
59 |
60 | DarkMode.decorators = [
61 | (Story) => (
62 |
63 |
64 |
65 | ),
66 | ];
67 |
--------------------------------------------------------------------------------
/src/lib/Radio/RadioGroup/RadioGroup.styles.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@lib/Theme/stitches.config';
2 |
3 | export const StyledRadioGroupWrapper = styled('div', {
4 | position: 'relative',
5 | boxSizing: 'border-box',
6 | display: 'flex',
7 | flexDirection: 'column',
8 | gap: '$2',
9 | });
10 |
--------------------------------------------------------------------------------
/src/lib/Radio/RadioGroup/RadioGroup.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import Radio from '@lib/Radio';
3 | import { render, screen } from '@testing-library/react';
4 | import userEvent from '@testing-library/user-event';
5 | import React from 'react';
6 |
7 | describe('components/RadioGroup', () => {
8 | it('matches snapshot', () => {
9 | const { asFragment } = render(
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | expect(asFragment()).toMatchSnapshot();
18 | });
19 | it('works as an uncontrolled component', async () => {
20 | const user = userEvent.setup();
21 | render(
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 |
30 | expect(screen.getByLabelText('Option A')).toBeChecked();
31 | await user.click(screen.getByLabelText('Option C'));
32 | expect(screen.getByLabelText('Option C')).toBeChecked();
33 | });
34 | it('works as a controlled component', async () => {
35 | let value = 'A';
36 | const user = userEvent.setup();
37 |
38 | render(
39 | ) =>
42 | (value = e.target.value)
43 | }
44 | >
45 |
46 |
47 |
48 |
49 |
50 | );
51 |
52 | expect(screen.getByLabelText('Option A')).toBeChecked();
53 | await user.click(screen.getByLabelText('Option C'));
54 | expect(value).toBe('C');
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/src/lib/Radio/RadioGroup/RadioGroup.tsx:
--------------------------------------------------------------------------------
1 | import { CSS, StandardColors } from '@lib/Theme/stitches.config';
2 | import { RadioProps } from '../Radio';
3 | import {
4 | PolymorphicRef,
5 | PolymorphicComponentPropWithRef,
6 | uuid,
7 | __DEV__,
8 | } from '@lib/Utils';
9 | import clsx from 'clsx';
10 | import React, { useMemo } from 'react';
11 |
12 | import { StyledRadioGroupWrapper } from './RadioGroup.styles';
13 |
14 | /**
15 | * RadioGroup is a helpful wrapper used to group Radio button components.
16 | */
17 | interface Props {
18 | /**
19 | * The content of the component.
20 | */
21 | children?:
22 | | Array>>
23 | | React.ReactElement>;
24 | /**
25 | * The default value. Used when component is not controlled.
26 | */
27 | defaultValue?: string;
28 | /**
29 | * ClassName applied to the component.
30 | * @default ''
31 | */
32 | className?: string;
33 | /**
34 | * The name used to reference the value of the control. If you do not provide this prop, it falls back to a randomly generated name.
35 | */
36 | name?: string;
37 | /**
38 | * Callback fired when a radio button is selected.
39 | */
40 | onChange?(e: React.ChangeEvent): void;
41 | /**
42 | * Value of the selected radio button.
43 | */
44 | value?: string;
45 | /**
46 | * Apply disabled state to all radio buttons in the radio group component
47 | * @default false
48 | */
49 | disabled?: boolean;
50 | /**
51 | * Color of radio buttons when active.
52 | */
53 | color?: StandardColors;
54 | /**
55 | * Size of each radio button.
56 | */
57 | size?: 'sm' | 'md' | 'lg';
58 | /**
59 | * Override default CSS style.
60 | */
61 | css?: CSS;
62 | }
63 |
64 | export type RadioGroupProps =
65 | PolymorphicComponentPropWithRef;
66 |
67 | export type RadioGroupComponent = ((
68 | props: RadioGroupProps
69 | ) => React.ReactElement | null) & { displayName?: string };
70 |
71 | const RadioGroup: RadioGroupComponent = React.forwardRef(
72 | (
73 | {
74 | children,
75 | defaultValue,
76 | className = '',
77 | name,
78 | onChange,
79 | value,
80 | disabled = false,
81 | color,
82 | size,
83 | as,
84 | css,
85 | ...props
86 | }: RadioGroupProps,
87 | ref?: PolymorphicRef
88 | ) => {
89 | const presetId = uuid('radio');
90 |
91 | const getName = useMemo(() => {
92 | if (name) {
93 | return name;
94 | }
95 | return presetId;
96 | }, [name]);
97 |
98 | const preClass = 'decaRadioGroup';
99 |
100 | return (
101 |
108 | {React.Children.map(
109 | children as React.ReactElement>,
110 | (child: React.ReactElement>) => {
111 | return React.cloneElement(child, {
112 | name: getName,
113 | onChange,
114 | initialSelect: child.props.value === defaultValue,
115 | selected: value ? child.props.value === value : undefined,
116 | disabled: disabled ? disabled : child.props.disabled,
117 | color,
118 | size,
119 | ...child.props,
120 | });
121 | }
122 | )}
123 |
124 | );
125 | }
126 | );
127 |
128 | if (__DEV__) {
129 | RadioGroup.displayName = 'DecaUI.RadioGroup';
130 | }
131 |
132 | export default RadioGroup;
133 |
--------------------------------------------------------------------------------
/src/lib/Radio/RadioGroup/__snapshots__/RadioGroup.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`components/RadioGroup matches snapshot 1`] = `
4 |
5 |
8 |
11 |
19 |
23 |
26 | Option A
27 |
28 |
29 |
32 |
39 |
43 |
46 | Option B
47 |
48 |
49 |
52 |
59 |
63 |
66 | Option C
67 |
68 |
69 |
72 |
79 |
83 |
86 | Option D
87 |
88 |
89 |
90 |
91 | `;
92 |
--------------------------------------------------------------------------------
/src/lib/Radio/RadioGroup/index.ts:
--------------------------------------------------------------------------------
1 | import RadioGroup from './RadioGroup';
2 | export * from './RadioGroup';
3 | export { RadioGroup };
4 | export default RadioGroup;
5 |
--------------------------------------------------------------------------------
/src/lib/Radio/__snapshots__/Radio.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`components/Radio matches snapshot 1`] = `
4 |
5 |
8 |
14 |
18 |
21 | Label Text
22 |
23 |
24 |
25 | `;
26 |
27 | exports[`components/Radio renders all colors 1`] = `
28 |
29 |
47 |
65 |
83 |
86 |
92 |
96 |
99 |
100 |
101 |
104 |
110 |
114 |
117 |
118 |
119 |
120 | `;
121 |
122 | exports[`components/Radio renders all sizes 1`] = `
123 |
124 |
127 |
133 |
137 |
140 |
141 |
142 |
145 |
151 |
155 |
158 |
159 |
160 |
163 |
169 |
173 |
176 |
177 |
178 |
179 | `;
180 |
--------------------------------------------------------------------------------
/src/lib/Radio/index.ts:
--------------------------------------------------------------------------------
1 | import Radio from './Radio';
2 | import RadioGroup from './RadioGroup';
3 |
4 | Radio.Group = RadioGroup;
5 |
6 | export * from './Radio';
7 | export { Radio };
8 | export default Radio;
9 |
--------------------------------------------------------------------------------
/src/lib/Switch/Switch.cy.tsx:
--------------------------------------------------------------------------------
1 | import { Test } from '../Utils';
2 | import _cyp from '../../../cypress';
3 | import React from 'react';
4 | import Switch from './Switch';
5 | import { standardColors } from '../Theme';
6 |
7 | describe('components/Switch', () => {
8 | describe('base', () => {
9 | describe('colors', () => {
10 | standardColors.map((color) =>
11 | it(color, () => {
12 | cy.mount( );
13 | cy.get('label')
14 | .before('background-color')
15 | .should('eq', Test.color('gray300'));
16 |
17 | cy.get('label').click();
18 |
19 | cy.wait(250);
20 |
21 | cy.get('label')
22 | .before('background-color')
23 | .should('eq', Test.color(color));
24 |
25 | cy.get('label')
26 | .after('background-color')
27 | .should('eq', Test.color('white'));
28 | })
29 | );
30 | });
31 | describe('disabled', () => {
32 | it('unselected', () => {
33 | cy.mount( );
34 | cy.get('label')
35 | .before('background-color')
36 | .should('eq', Test.color('gray200'));
37 |
38 | cy.get('label')
39 | .after('background-color')
40 | .should('eq', Test.color('gray400'));
41 | });
42 | it('selected', () => {
43 | cy.mount( );
44 | cy.get('label');
45 | cy.get('label')
46 | .before('background-color')
47 | .should('eq', Test.color('gray200'));
48 |
49 | cy.get('label')
50 | .after('background-color')
51 | .should('eq', Test.color('gray400'));
52 | });
53 | });
54 | });
55 | describe('dark mode', () => {
56 | describe('colors', () => {
57 | standardColors.map((color) =>
58 | it(color, () => {
59 | cy.darkMount( );
60 | cy.get('label')
61 | .before('background-color')
62 | .should('eq', Test.color('gray800'));
63 |
64 | cy.get('label').click();
65 |
66 | cy.wait(250);
67 |
68 | cy.get('label')
69 | .before('background-color')
70 | .should('eq', Test.color(color));
71 |
72 | cy.get('label')
73 | .after('background-color')
74 | .should('eq', Test.color('black'));
75 | })
76 | );
77 | });
78 | describe('disabled', () => {
79 | it('unselected', () => {
80 | cy.darkMount( );
81 | cy.get('label')
82 | .before('background-color')
83 | .should('eq', Test.color('gray700'));
84 |
85 | cy.get('label')
86 | .after('background-color')
87 | .should('eq', Test.color('gray600'));
88 |
89 | cy.get('label').after('opacity').should('eq', '0.4');
90 | });
91 | it('selected', () => {
92 | cy.darkMount( );
93 | cy.get('label')
94 | .before('background-color')
95 | .should('eq', Test.color('gray700'));
96 |
97 | cy.get('label')
98 | .after('background-color')
99 | .should('eq', Test.color('gray600'));
100 |
101 | cy.get('label').after('opacity').should('eq', '0.4');
102 | });
103 | });
104 | });
105 | });
106 |
--------------------------------------------------------------------------------
/src/lib/Switch/Switch.stories.tsx:
--------------------------------------------------------------------------------
1 | import { DecaUIProvider } from '@lib/Theme';
2 | import { ComponentMeta, ComponentStory } from '@storybook/react';
3 | import React from 'react';
4 |
5 | import Switch from './Switch';
6 |
7 | export default {
8 | title: 'Switch',
9 | component: Switch,
10 | } as ComponentMeta;
11 |
12 | const Template: ComponentStory = (args) => ;
13 |
14 | export const Default = Template.bind({});
15 | Default.args = {
16 | label: 'Label',
17 | size: 'md',
18 | color: 'primary',
19 | disabled: false,
20 | css: {},
21 | className: '',
22 | initialToggle: true,
23 | required: false,
24 | };
25 |
26 | export const NoLabel = Template.bind({});
27 | NoLabel.args = {
28 | ...Default.args,
29 | label: '',
30 | };
31 |
32 | export const WithTheme = Template.bind({});
33 |
34 | WithTheme.args = { ...Default.args };
35 | WithTheme.decorators = [
36 | (Story) => (
37 |
44 |
45 |
46 | ),
47 | ];
48 |
49 | export const DarkMode = Template.bind({});
50 |
51 | DarkMode.args = { ...Default.args };
52 | DarkMode.parameters = { backgrounds: { default: 'dark' } };
53 |
54 | DarkMode.decorators = [
55 | (Story) => (
56 |
57 |
58 |
59 | ),
60 | ];
61 |
--------------------------------------------------------------------------------
/src/lib/Switch/Switch.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, act } from '@testing-library/react';
2 | import React from 'react';
3 |
4 | import Switch from './Switch';
5 |
6 | describe('components/Switch', () => {
7 | it('matches screenshot', () => {
8 | const { asFragment } = render( );
9 | expect(asFragment()).toMatchSnapshot();
10 | });
11 | it('onChange event fires', () => {
12 | const mockFn = jest.fn();
13 | const utils = render( );
14 | const input = utils.getByLabelText('Label Text');
15 | act(() => {
16 | input.click();
17 | });
18 | expect(mockFn.mock.calls.length).toBe(1);
19 | });
20 | it('should ignore events when disabled', () => {
21 | const mockFn = jest.fn();
22 | const utils = render(
23 |
24 | );
25 | const input = utils.getByLabelText('Label Text');
26 | act(() => {
27 | input.click();
28 | });
29 | expect(mockFn.mock.calls.length).toBe(0);
30 | });
31 | it('renders all colors', () => {
32 | const { asFragment } = render(
33 | <>
34 |
35 |
36 |
37 |
38 |
39 | >
40 | );
41 | expect(asFragment()).toMatchSnapshot();
42 | });
43 | it('renders all sizes', () => {
44 | const { asFragment } = render(
45 | <>
46 |
47 |
48 |
49 | >
50 | );
51 | expect(asFragment()).toMatchSnapshot();
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/src/lib/Switch/Switch.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeContext } from '@lib/Theme';
2 | import { CSS, StandardColors } from '@lib/Theme/stitches.config';
3 | import {
4 | Modify,
5 | PolymorphicRef,
6 | PolymorphicComponentPropWithRef,
7 | uuid,
8 | __DEV__,
9 | } from '@lib/Utils';
10 | import clsx from 'clsx';
11 | import React, { useState, useMemo } from 'react';
12 |
13 | import {
14 | StyledSwitchWrapper,
15 | StyledSwitchInput,
16 | StyledSwitchLabel,
17 | } from './Switch.styles';
18 | /**
19 | * Switches are an alternative to the checkbox component. You can switch between enabled or disabled states.
20 | */
21 | interface Props
22 | extends Modify<
23 | React.ComponentPropsWithRef<'input'>,
24 | {
25 | /**
26 | * Size of the component.
27 | * @default md
28 | */
29 | size?: 'sm' | 'md' | 'lg';
30 | }
31 | > {
32 | /**
33 | * Size of the component.
34 | * @default md
35 | */
36 | size?: 'sm' | 'md' | 'lg';
37 | /**
38 | * Text label for the component.
39 | */
40 | label?: string;
41 | /**
42 | * ClassName applied to the component.
43 | * @default ''
44 | */
45 | className?: string;
46 | /**
47 | * Disabled state applied to the component.
48 | * @default false
49 | */
50 | disabled?: boolean;
51 | /**
52 | * Override default CSS style.
53 | */
54 | css?: CSS;
55 | /**
56 | * Color of radio button when active.
57 | * @default primary
58 | */
59 | color?: StandardColors;
60 | /**
61 | * The value assigned to the switch component.
62 | */
63 | value?: string;
64 | /**
65 | * Callback fired when a switch is enabled or disabled
66 | */
67 | onChange?(e: React.ChangeEvent): void;
68 | /**
69 | * Callback fired when a switch is focused
70 | */
71 | onFocus?(e: React.ChangeEvent): void;
72 | /**
73 | * Name is used as an identifier in a form.
74 | */
75 | name?: string;
76 | /**
77 | * Whether or not the switch is initially toggled.
78 | */
79 | initialToggle?: boolean;
80 | /**
81 | * Whether or not the switch is toggled.
82 | */
83 | toggled?: boolean;
84 | /**
85 | * Required checkbox prop.
86 | * @default false
87 | */
88 | required?: boolean;
89 | }
90 |
91 | export type SwitchProps =
92 | PolymorphicComponentPropWithRef;
93 |
94 | export type SwitchComponent = ((
95 | props: SwitchProps
96 | ) => React.ReactElement | null) & { displayName?: string };
97 |
98 | const Switch: SwitchComponent = React.forwardRef(
99 | (
100 | {
101 | size = 'md',
102 | label,
103 | className,
104 | disabled = false,
105 | as,
106 | css,
107 | color = 'primary',
108 | required = false,
109 | toggled,
110 | initialToggle = false,
111 | onChange,
112 | name,
113 | value,
114 | }: SwitchProps,
115 | ref?: PolymorphicRef
116 | ) => {
117 | const [selfToggled, setSelfToggled] = useState(initialToggle);
118 |
119 | const switchId = uuid('switch');
120 |
121 | const isControlledComponent = useMemo(
122 | () => toggled !== undefined,
123 | [toggled]
124 | );
125 |
126 | const changeHandler = (e: React.ChangeEvent) => {
127 | if (disabled) return;
128 | if (!isControlledComponent) {
129 | setSelfToggled(e.target.checked);
130 | }
131 | onChange && onChange(e);
132 | };
133 |
134 | const preClass = 'decaSwitch';
135 |
136 | const { dark } = React.useContext(ThemeContext);
137 |
138 | return (
139 |
140 |
156 |
166 | {label && label}
167 |
168 |
169 | );
170 | }
171 | );
172 |
173 | if (__DEV__) {
174 | Switch.displayName = 'DecaUI.Switch';
175 | }
176 |
177 | export default Switch;
178 |
--------------------------------------------------------------------------------
/src/lib/Switch/__snapshots__/Switch.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`components/Switch matches screenshot 1`] = `
4 |
5 |
8 |
14 |
18 | Label Text
19 |
20 |
21 |
22 | `;
23 |
24 | exports[`components/Switch renders all colors 1`] = `
25 |
26 |
29 |
35 |
39 |
40 |
43 |
49 |
53 |
54 |
57 |
63 |
67 |
68 |
71 |
77 |
81 |
82 |
85 |
91 |
95 |
96 |
97 | `;
98 |
99 | exports[`components/Switch renders all sizes 1`] = `
100 |
101 |
104 |
110 |
114 |
115 |
118 |
124 |
128 |
129 |
132 |
138 |
142 |
143 |
144 | `;
145 |
--------------------------------------------------------------------------------
/src/lib/Switch/index.ts:
--------------------------------------------------------------------------------
1 | import Switch from './Switch';
2 | export * from './Switch';
3 | export { Switch };
4 | export default Switch;
5 |
--------------------------------------------------------------------------------
/src/lib/Text/Text.stories.tsx:
--------------------------------------------------------------------------------
1 | import { DecaUIProvider } from '@lib/Theme';
2 | import { ComponentMeta, ComponentStory } from '@storybook/react';
3 | import React from 'react';
4 |
5 | import Text from './Text';
6 |
7 | export default {
8 | title: 'Text',
9 | component: Text,
10 | } as ComponentMeta;
11 |
12 | const Template: ComponentStory = (args) => ;
13 |
14 | const showcaseText = 'Almost before we knew it, we had left the ground.';
15 |
16 | export const Header = Template.bind({});
17 |
18 | Header.args = {
19 | as: 'h1',
20 | children: showcaseText,
21 | size: 'h1',
22 | mono: false,
23 | };
24 |
25 | export const Typescale = () => {
26 | return (
27 | <>
28 | {showcaseText}
29 | {showcaseText}
30 | {showcaseText}
31 | {showcaseText}
32 | {showcaseText}
33 | {showcaseText}
34 |
35 | {showcaseText}
36 |
37 | {showcaseText}
38 | {showcaseText}
39 | >
40 | );
41 | };
42 |
43 | export const WithTheme = Template.bind({});
44 |
45 | WithTheme.args = { ...Header.args };
46 | WithTheme.decorators = [
47 | (Story) => (
48 |
55 |
56 |
57 | ),
58 | ];
59 |
60 | export const DarkMode = Template.bind({});
61 |
62 | DarkMode.args = { ...Header.args };
63 | DarkMode.parameters = { backgrounds: { default: 'dark' } };
64 |
65 | DarkMode.decorators = [
66 | (Story) => (
67 |
68 |
69 |
70 | ),
71 | ];
72 |
--------------------------------------------------------------------------------
/src/lib/Text/Text.styles.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@lib/Theme/stitches.config';
2 |
3 | const StyledText = styled('p', {
4 | m: '$n',
5 | variants: {
6 | weight: {
7 | hairline: {
8 | fontWeight: '$hairline',
9 | },
10 | thin: {
11 | fontWeight: '$thin',
12 | },
13 | light: {
14 | fontWeight: '$light',
15 | },
16 | normal: {
17 | fontWeight: '$normal',
18 | },
19 | medium: {
20 | fontWeight: '$medium',
21 | },
22 | semibold: {
23 | fontWeight: '$semibold',
24 | },
25 | bold: {
26 | fontWeight: '$bold',
27 | },
28 | extrabold: {
29 | fontWeight: '$extrabold',
30 | },
31 | black: {
32 | fontWeight: '$black',
33 | },
34 | },
35 | as: {
36 | h1: {
37 | fontSize: '$h1',
38 | lineHeight: '$6',
39 | },
40 | h2: {
41 | fontSize: '$h2',
42 | lineHeight: '$5',
43 | },
44 | h3: {
45 | fontSize: '$h3',
46 | lineHeight: '$4',
47 | },
48 | h4: {
49 | fontSize: '$h4',
50 | lineHeight: '$3',
51 | },
52 | h5: {
53 | fontSize: '$h5',
54 | lineHeight: '$2',
55 | },
56 | h6: {
57 | fontSize: '$h6',
58 | lineHeight: '$2',
59 | },
60 | p: {
61 | fontSize: '$body',
62 | lineHeight: '$1',
63 | },
64 | span: {
65 | fontSize: '$body',
66 | lineHeight: '$1',
67 | },
68 | blockquote: {
69 | fontSize: '$body',
70 | lineHeight: '$1',
71 | ml: '$4',
72 | },
73 | b: {
74 | fontSize: '$body',
75 | lineHeight: '$1',
76 | },
77 | small: {
78 | fontSize: '$caption',
79 | lineHeight: '$0',
80 | },
81 | del: {
82 | fontSize: '$body',
83 | lineHeight: '$1',
84 | },
85 | i: {
86 | fontSize: '$body',
87 | lineHeight: '$1',
88 | },
89 | em: {
90 | fontSize: '$body',
91 | lineHeight: '$1',
92 | },
93 | },
94 | size: {
95 | h1: {
96 | fontSize: '$h1',
97 | lineHeight: '$6',
98 | },
99 | h2: {
100 | fontSize: '$h2',
101 | lineHeight: '$5',
102 | },
103 | h3: {
104 | fontSize: '$h3',
105 | lineHeight: '$4',
106 | },
107 | h4: {
108 | fontSize: '$h4',
109 | lineHeight: '$3',
110 | },
111 | h5: {
112 | fontSize: '$h5',
113 | lineHeight: '$2',
114 | },
115 | h6: {
116 | fontSize: '$h6',
117 | lineHeight: '$2',
118 | },
119 | body: {
120 | fontSize: '$body',
121 | lineHeight: '$1',
122 | },
123 | bodySm: {
124 | fontSize: '$bodySm',
125 | lineHeight: '$1',
126 | },
127 | caption: {
128 | fontSize: '$caption',
129 | lineHeight: '$1',
130 | },
131 | footnote: {
132 | fontSize: '$footnote',
133 | lineHeight: '$0',
134 | },
135 | },
136 | lineHeight: {
137 | n: {
138 | lineHeight: '$n',
139 | },
140 | 0: {
141 | lineHeight: '$0',
142 | },
143 | 1: {
144 | lineHeight: '$1',
145 | },
146 | 2: {
147 | lineHeight: '$2',
148 | },
149 | 3: {
150 | lineHeight: '$3',
151 | },
152 | 4: {
153 | lineHeight: '$4',
154 | },
155 | 5: {
156 | lineHeight: '$5',
157 | },
158 | 6: {
159 | lineHeight: '$6',
160 | },
161 | },
162 | center: {
163 | true: {
164 | textAlign: 'center',
165 | },
166 | false: {},
167 | },
168 | isDark: {
169 | true: {
170 | color: '$text',
171 | },
172 | false: {},
173 | },
174 | mono: {
175 | true: {
176 | fontFamily: '$mono',
177 | },
178 | false: {
179 | fontFamily: '$normal',
180 | },
181 | },
182 | },
183 | });
184 |
185 | export default StyledText;
186 |
--------------------------------------------------------------------------------
/src/lib/Text/Text.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import '@testing-library/jest-dom';
3 | import React from 'react';
4 |
5 | import Text from './Text';
6 |
7 | describe('components/Text', () => {
8 | it('renders content', () => {
9 | const { getByText } = render(Children );
10 | expect(getByText('Children')).toBeInTheDocument();
11 | });
12 | it('matches snapshot', () => {
13 | const { asFragment } = render( );
14 | expect(asFragment()).toMatchSnapshot();
15 | });
16 | it('renders all variants', () => {
17 | const { asFragment } = render(
18 | <>
19 | Children
20 | Children
21 | Children
22 | Children
23 | Children
24 | Children
25 | Children
26 | Children
27 | Children
28 | Children
29 | Children
30 | Children
31 | Children
32 | Children
33 | >
34 | );
35 | expect(asFragment()).toMatchSnapshot();
36 | });
37 | it('renders all sizes', () => {
38 | const { asFragment } = render(
39 | <>
40 | Children
41 | Children
42 | Children
43 | Children
44 | Children
45 | Children
46 | Children
47 | Children
48 | Children
49 | Children
50 | >
51 | );
52 | expect(asFragment()).toMatchSnapshot();
53 | });
54 | it('renders all lineHeights', () => {
55 | const { asFragment } = render(
56 | <>
57 | Children
58 | Children
59 | Children
60 | Children
61 | Children
62 | Children
63 | Children
64 | Children
65 | >
66 | );
67 | expect(asFragment()).toMatchSnapshot();
68 | });
69 | it('renders sizes with variants', () => {
70 | const { asFragment } = render(
71 |
72 | Children
73 |
74 | );
75 | expect(asFragment()).toMatchSnapshot();
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/src/lib/Text/Text.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeContext } from '@lib/Theme';
2 | import { CSS } from '@lib/Theme/stitches.config';
3 | import {
4 | PolymorphicRef,
5 | PolymorphicComponentPropWithRef,
6 | AnyObject,
7 | __DEV__,
8 | } from '@lib/Utils';
9 | import React from 'react';
10 |
11 | import StyledText from './Text.styles';
12 |
13 | export type TextAs =
14 | | 'h1'
15 | | 'h2'
16 | | 'h3'
17 | | 'h4'
18 | | 'h5'
19 | | 'h6'
20 | | 'p'
21 | | 'span'
22 | | 'blockquote'
23 | | 'b'
24 | | 'small'
25 | | 'del'
26 | | 'i'
27 | | 'em';
28 |
29 | export type TextWeight =
30 | | 'hairline'
31 | | 'thin'
32 | | 'light'
33 | | 'normal'
34 | | 'medium'
35 | | 'semibold'
36 | | 'bold'
37 | | 'extrabold'
38 | | 'black';
39 |
40 | export type TextSize =
41 | | 'h1'
42 | | 'h2'
43 | | 'h3'
44 | | 'h4'
45 | | 'h5'
46 | | 'h6'
47 | | 'body'
48 | | 'bodySm'
49 | | 'caption'
50 | | 'footnote'
51 | | AnyObject;
52 |
53 | export type TextLineHeight = 'n' | '0' | '1' | '2' | '3' | '4' | '5' | '6';
54 |
55 | /**
56 | * The Text component is the used to render text and paragraphs within an interface
57 | */
58 | interface Props {
59 | /**
60 | * Changes which tag component outputs.
61 | */
62 | as?: TextAs;
63 | /**
64 | * Override default CSS style.
65 | */
66 | css?: CSS;
67 | /**
68 | * The content of the component.
69 | */
70 | children?: React.ReactNode | undefined;
71 | /**
72 | * Font weight of the text.
73 | */
74 | weight?: TextWeight;
75 | /**
76 | * Custom size for the text.
77 | */
78 | size?: TextSize;
79 | /**
80 | * Center text.
81 | */
82 | center?: boolean;
83 | /**
84 | * Custom line height for the text.
85 | */
86 | lineHeight?: TextLineHeight;
87 | /**
88 | * Should text be mono font-family.
89 | * @default false;
90 | */
91 | mono?: boolean;
92 | }
93 |
94 | export type TextProps =
95 | PolymorphicComponentPropWithRef;
96 |
97 | export type TextComponent = ((
98 | props: TextProps
99 | ) => React.ReactElement | null) & { displayName?: string };
100 |
101 | const Text: TextComponent = React.forwardRef(
102 | (
103 | {
104 | as,
105 | css,
106 | children,
107 | weight,
108 | size,
109 | lineHeight,
110 | center,
111 | mono = false,
112 | ...textProps
113 | }: TextProps,
114 | ref?: PolymorphicRef
115 | ) => {
116 | const preClass = 'decaText';
117 |
118 | const { dark } = React.useContext(ThemeContext);
119 |
120 | return (
121 |
134 | {children}
135 |
136 | );
137 | }
138 | );
139 |
140 | if (__DEV__) {
141 | Text.displayName = 'DecaUI.Text';
142 | }
143 |
144 | export default Text;
145 |
--------------------------------------------------------------------------------
/src/lib/Text/index.ts:
--------------------------------------------------------------------------------
1 | import Text from './Text';
2 | export * from './Text';
3 | export { Text };
4 | export default Text;
5 |
--------------------------------------------------------------------------------
/src/lib/Theme/DecaUIProvider.test.tsx:
--------------------------------------------------------------------------------
1 | import { cssVar } from 'polished';
2 | import { render } from '@testing-library/react';
3 | import React from 'react';
4 | import DecaUIProvider from './DecaUIProvider';
5 |
6 | describe('components/DecaUIProvider', () => {
7 | it('matches snapshot', () => {
8 | const { asFragment } = render( );
9 | expect(asFragment()).toMatchSnapshot();
10 | });
11 | it('can change cssVars', () => {
12 | render(
13 |
16 | );
17 | expect(cssVar('--colors-primary', '#FFFFFF'));
18 | });
19 | it('text color stays black in light mode', () => {
20 | render( );
21 | expect(cssVar('--colors-text', '#FFFFFF'));
22 | });
23 | it('dark mode changes default text color', () => {
24 | render( );
25 | expect(cssVar('--colors-text', '#000000'));
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/lib/Theme/DecaUIProvider.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from '@lib/Theme/stitches.config';
2 | import { createPalette, __DEV__ } from '@lib/Utils';
3 | import React, { useMemo } from 'react';
4 | import { SSRProvider } from '@react-aria/ssr';
5 |
6 | import { createTheme, globalCss } from './stitches.config';
7 |
8 | type ThemeValue = { [token in number | string]: boolean | number | string };
9 |
10 | export type Theme> = {
11 | fonts?: ThemeValue;
12 | fontSizes?: ThemeValue;
13 | fontWeights?: ThemeValue;
14 | lineHeights?: ThemeValue;
15 | letterSpacings?: ThemeValue;
16 | space?: ThemeValue;
17 | radii?: ThemeValue;
18 | zIndices?: ThemeValue;
19 | borderWeights?: ThemeValue;
20 | colors?: ThemeValue;
21 | shadows?: ThemeValue;
22 | dropShadows?: ThemeValue;
23 | transitions?: ThemeValue;
24 | breakpoints?: ThemeValue;
25 | } & {
26 | [Scale in keyof T]: {
27 | [Token in keyof T[Scale]]: T[Scale][Token] extends boolean | number | string
28 | ? T[Scale][Token]
29 | : boolean | number | string;
30 | };
31 | };
32 |
33 | export interface DecaUIProviderProps {
34 | /**
35 | * Set dark or light mode.
36 | */
37 | mode?: 'light' | 'dark';
38 | /**
39 | * Custom theme object.
40 | */
41 | theme?: Theme;
42 | /**
43 | * The content of the component.
44 | */
45 | children?: React.ReactNode | undefined;
46 | /**
47 | * Do not add CSS baseline.
48 | */
49 | noBaseline?: boolean;
50 | /**
51 | * Is the provider being used at the highest level of the React App
52 | */
53 | root?: boolean;
54 | }
55 |
56 | const globalBaseline = globalCss({
57 | /* Box sizing rules */
58 | '*, *::before, *::after': {
59 | boxSizing: 'border-box',
60 | },
61 |
62 | /* Remove default margin */
63 | 'body, h1, h2, h3, h4, p, figure, blockquote, dl, dd': {
64 | margin: 0,
65 | },
66 |
67 | /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
68 | "ul[role='list'], ol[role='list']": {
69 | listStyle: 'none',
70 | },
71 |
72 | /* Set core root defaults */
73 | 'html:focus-within': {
74 | scrollBehavior: 'smooth',
75 | },
76 |
77 | /* Set core body defaults */
78 | body: {
79 | minHeight: '100vh',
80 | textRendering: 'optimizeSpeed',
81 | lineHeight: 1.5,
82 | },
83 |
84 | /* A elements that don't have a class get default styles */
85 | 'a:not([class])': {
86 | textDecorationSkipInk: 'auto',
87 | },
88 |
89 | /* Make images easier to work with */
90 | 'img, picture': {
91 | maxWidth: '100%',
92 | display: 'block',
93 | },
94 |
95 | /* Inherit fonts for inputs and buttons */
96 | 'input, button, textarea, select': {
97 | font: 'inherit',
98 | },
99 |
100 | /* Remove all animations, transitions and smooth scroll for people that prefer not to see them */
101 | '@media (prefers-reduced-motion: reduce)': {
102 | 'html:focus-within': {
103 | scrollBehavior: 'auto',
104 | },
105 |
106 | '*, *::before, *::after': {
107 | animationDuration: '0.01ms!important',
108 | animationIterationCount: '1!important',
109 | transitionDuration: '0.01ms!important',
110 | scrollBehavior: 'auto!important',
111 | },
112 | },
113 | });
114 |
115 | export const ThemeContext = React.createContext({ dark: false });
116 |
117 | const ThemeProvider = styled('div', {});
118 |
119 | const DecaUIProvider: React.FC<
120 | React.PropsWithChildren
121 | > = ({ mode, theme, children, noBaseline, root = true }) => {
122 | const darkMode = useMemo(() => {
123 | return mode === 'dark' ? true : false;
124 | }, [mode]);
125 |
126 | const modifiedTheme = useMemo(() => {
127 | if (theme && theme.colors) {
128 | return {
129 | ...theme,
130 | colors: {
131 | ...createPalette(theme.colors as Record),
132 | text: darkMode ? '$white' : '$black',
133 | },
134 | };
135 | } else {
136 | return { ...theme, colors: { text: darkMode ? '$white' : '$black' } };
137 | }
138 | }, [theme]);
139 |
140 | if (!noBaseline) {
141 | globalBaseline();
142 | }
143 |
144 | const userTheme = useMemo(() => createTheme(modifiedTheme as Theme), [theme]);
145 |
146 | if (!root) {
147 | return {children}
;
148 | }
149 |
150 | return (
151 |
152 |
153 |
154 | {children}
155 |
156 |
157 |
158 | );
159 | };
160 |
161 | if (__DEV__) {
162 | DecaUIProvider.displayName = 'DecaUI.Provider';
163 | }
164 |
165 | export default DecaUIProvider;
166 |
--------------------------------------------------------------------------------
/src/lib/Theme/__snapshots__/DecaUIProvider.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`components/DecaUIProvider matches snapshot 1`] = `
4 |
5 |
9 |
10 | `;
11 |
--------------------------------------------------------------------------------
/src/lib/Theme/index.ts:
--------------------------------------------------------------------------------
1 | import DecaUIProvider from './DecaUIProvider';
2 | export * from './stitches.config';
3 | export * from './DecaUIProvider';
4 | export { DecaUIProvider };
5 | export default DecaUIProvider;
6 |
--------------------------------------------------------------------------------
/src/lib/Utils/color.ts:
--------------------------------------------------------------------------------
1 | import { theme } from '@lib/Theme/stitches.config';
2 | import { darken, transparentize, readableColor } from 'polished';
3 |
4 | export const getStaticColor = (varColor: string): string => {
5 | return theme.colors[varColor as keyof typeof theme.colors].value;
6 | };
7 |
8 | const generatePalette = (color: string[]) => {
9 | const colors = [];
10 |
11 | colors.push(color as string[]);
12 | colors.push([`${color[0]}-darken-1`, darken(0.055, color[1] as string)]);
13 | colors.push([`${color[0]}-darken-2`, darken(0.09, color[1] as string)]);
14 | colors.push([`${color[0]}-darken-3`, darken(0.125, color[1] as string)]);
15 | colors.push([`${color[0]}-darken-4`, darken(0.256, color[1] as string)]);
16 | colors.push([
17 | `${color[0]}-lighten-1`,
18 | transparentize(0.15, color[1] as string),
19 | ]);
20 | colors.push([
21 | `${color[0]}-lighten-2`,
22 | transparentize(0.25, color[1] as string),
23 | ]);
24 | colors.push([
25 | `${color[0]}-lighten-3`,
26 | transparentize(0.5, color[1] as string),
27 | ]);
28 | colors.push([
29 | `${color[0]}-lighten-4`,
30 | transparentize(0.65, color[1] as string),
31 | ]);
32 | colors.push([
33 | `${color[0]}-lighten-5`,
34 | transparentize(0.78, color[1] as string),
35 | ]);
36 | colors.push([
37 | `${color[0]}-lighten-6`,
38 | transparentize(0.85, color[1] as string),
39 | ]);
40 | colors.push([
41 | `${color[0]}-lighten-7`,
42 | transparentize(0.9, color[1] as string),
43 | ]);
44 | colors.push([
45 | `${color[0]}-lighten-8`,
46 | transparentize(0.99, color[1] as string),
47 | ]);
48 | colors.push([
49 | `${color[0]}-readable`,
50 | readableColor(
51 | darken(0.225, color[1] as string),
52 | getStaticColor('white'),
53 | getStaticColor('black')
54 | ),
55 | ]);
56 |
57 | return colors;
58 | };
59 |
60 | export const createPalette = (colorObj: Record) => {
61 | const modifiedColors: Array> = [];
62 | Object.entries(colorObj).map((color) => {
63 | // variable color
64 | if ((color[1] as string).charAt(0) === '$') {
65 | color[1] = getStaticColor((color[1] as string).substring(1));
66 | }
67 | modifiedColors.push(...generatePalette(color));
68 | });
69 | return Object.fromEntries(modifiedColors);
70 | };
71 |
72 | export const hexRgb = (
73 | hex: string,
74 | options: Record = {}
75 | ): Record => {
76 | const hexCharacters = 'a-f\\d';
77 | const match3or4Hex = `#?[${hexCharacters}]{3}[${hexCharacters}]?`;
78 | const match6or8Hex = `#?[${hexCharacters}]{6}([${hexCharacters}]{2})?`;
79 | const nonHexChars = new RegExp(`[^#${hexCharacters}]`, 'gi');
80 | const validHexSize = new RegExp(`^${match3or4Hex}$|^${match6or8Hex}$`, 'i');
81 | if (
82 | typeof hex !== 'string' ||
83 | nonHexChars.test(hex) ||
84 | !validHexSize.test(hex)
85 | ) {
86 | throw new TypeError('Expected a valid hex string');
87 | }
88 |
89 | hex = hex.replace(/^#/, '');
90 | let alphaFromHex = 1;
91 |
92 | if (hex.length === 8) {
93 | alphaFromHex = Number.parseInt(hex.slice(6, 8), 16) / 255;
94 | hex = hex.slice(0, 6);
95 | }
96 |
97 | if (hex.length === 4) {
98 | alphaFromHex = Number.parseInt(hex.slice(3, 4).repeat(2), 16) / 255;
99 | hex = hex.slice(0, 3);
100 | }
101 |
102 | if (hex.length === 3) {
103 | hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
104 | }
105 |
106 | const number = Number.parseInt(hex, 16);
107 | const red = number >> 16;
108 | const green = (number >> 8) & 255;
109 | const blue = number & 255;
110 | const alpha =
111 | typeof options.alpha === 'number' ? options.alpha : alphaFromHex;
112 |
113 | return { red, green, blue, alpha };
114 | };
115 |
--------------------------------------------------------------------------------
/src/lib/Utils/env.ts:
--------------------------------------------------------------------------------
1 | export const __DEV__ = process.env.NODE_ENV === 'development';
2 |
--------------------------------------------------------------------------------
/src/lib/Utils/hooks.ts:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect } from 'react';
2 |
3 | type DOMException = React.MutableRefObject;
4 | /**
5 | * Executes callback function if window is clicked outside of specified element. Exceptions argument allows for other specified elements to be clicked without the callback function executing
6 | */
7 | export const useClickOutside = (
8 | handler: () => void,
9 | exceptions?: Array | undefined>
10 | ) => {
11 | const domNode = useRef(null);
12 |
13 | useEffect(() => {
14 | const conditionalHandler = (e: MouseEvent) => {
15 | if (
16 | exceptions !== undefined &&
17 | (exceptions as Array).length !== 0
18 | ) {
19 | (exceptions as Array).map((i) => {
20 | if (
21 | i.current &&
22 | !i.current.isSameNode(e.target as Node) &&
23 | domNode.current &&
24 | !domNode.current.contains(e.target as Node)
25 | ) {
26 | handler();
27 | }
28 | });
29 | } else {
30 | if (domNode.current && !domNode.current.contains(e.target as Node)) {
31 | handler();
32 | }
33 | }
34 | };
35 |
36 | document.addEventListener('mousedown', conditionalHandler);
37 | return () => {
38 | document.removeEventListener('mousedown', conditionalHandler);
39 | };
40 | });
41 |
42 | return domNode;
43 | };
44 |
--------------------------------------------------------------------------------
/src/lib/Utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types';
2 | export * from './refs';
3 | export * from './hooks';
4 | export * from './color';
5 | export * from './random';
6 | export * from './env';
7 | export * from './test';
8 |
--------------------------------------------------------------------------------
/src/lib/Utils/random.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from 'uuid';
2 |
3 | /**
4 | * Generates random UUID
5 | */
6 | export const uuid = (prefix?: string) => {
7 | const genUUID = uuidv4();
8 | if (prefix) {
9 | return `${prefix}-${genUUID}`;
10 | } else {
11 | return genUUID;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/lib/Utils/refs.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export type ReactRef =
4 | | React.Ref
5 | | React.RefObject
6 | | React.MutableRefObject;
7 |
8 | /**
9 | * Assigns a value to a ref function or object
10 | *
11 | * @param ref the ref to assign to
12 | * @param value the value
13 | */
14 | export function assignRef(ref: ReactRef | undefined, value: T) {
15 | if (ref == null) return;
16 |
17 | if (typeof ref === 'function') {
18 | ref(value);
19 | return;
20 | }
21 |
22 | try {
23 | (ref as React.MutableRefObject).current = value;
24 | } catch (error) {
25 | throw new Error(`Cannot assign value '${value}' to ref '${ref}'`);
26 | }
27 | }
28 |
29 | /**
30 | * Combine multiple React refs into a single ref function.
31 | * This is used mostly when you need to allow consumers forward refs to
32 | * internal components
33 | *
34 | * @param refs refs to assign to value to
35 | */
36 | export function mergeRefs(...refs: (ReactRef | undefined)[]) {
37 | return (node: T | null) => {
38 | refs.forEach((ref) => assignRef(ref, node));
39 | };
40 | }
41 |
--------------------------------------------------------------------------------
/src/lib/Utils/test.ts:
--------------------------------------------------------------------------------
1 | import { cssVar } from 'polished';
2 | import { hexRgb } from './color';
3 |
4 | export const remToPx = (remVal: string | number) => {
5 | return parseFloat((remVal as string).slice(0, -3)) * 16 + 'px';
6 | };
7 |
8 | // all of these methods are helper functions for cypress test files
9 | export class Test {
10 | // turn hex to rgb or rgba
11 | static color(inputColor: string) {
12 | const cssColor = cssVar(`--colors-${inputColor}`);
13 | if ((cssColor as string).charAt(0) === '#') {
14 | const rgbObj = hexRgb(cssColor as string);
15 | const r = rgbObj.red;
16 | const g = rgbObj.green;
17 | const b = rgbObj.blue;
18 | const a = rgbObj.alpha;
19 | if ((cssColor as string).length > 7) {
20 | return `rgba(${r}, ${g}, ${b}, ${a})`;
21 | }
22 | return `rgb(${r}, ${g}, ${b})`;
23 | }
24 | return cssColor;
25 | }
26 |
27 | static size(inputSize: string) {
28 | const inputArr = inputSize.split(' ');
29 | if (inputArr.length === 1) {
30 | const cssSpace = cssVar(`--sizes-${inputSize}`);
31 | return remToPx(cssSpace);
32 | }
33 | for (let i = 0; i < inputArr.length; i++) {
34 | if (Number.isInteger(parseInt(inputArr[i]))) {
35 | inputArr[i] = (cssVar(`--sizes-${inputArr[i]}`) as string).slice(0, -3);
36 | }
37 | }
38 | return eval(inputArr.join('')) * 16 + 'px';
39 | }
40 |
41 | static space(inputSpace: string) {
42 | const inputArr = inputSpace.split(' ');
43 | if (inputArr.length === 1) {
44 | const cssSpace = cssVar(`--space-${inputSpace}`);
45 | return remToPx(cssSpace);
46 | }
47 | for (let i = 0; i < inputArr.length; i++) {
48 | if (Number.isInteger(parseInt(inputArr[i]))) {
49 | inputArr[i] = (cssVar(`--space-${inputArr[i]}`) as string).slice(0, -3);
50 | }
51 | }
52 | return eval(inputArr.join('')) * 16 + 'px';
53 | }
54 |
55 | static fontSize(inputFontSize: string) {
56 | const cssFontSize = cssVar(`--fontSizes-${inputFontSize}`);
57 | return remToPx(cssFontSize);
58 | }
59 |
60 | static borderRadius(inputBorderRadius: string) {
61 | const cssBorderRadius = cssVar(`--radii-${inputBorderRadius}`);
62 | return cssBorderRadius;
63 | }
64 |
65 | static font(inputFont: string) {
66 | const cssFont = cssVar(`--fonts-${inputFont}`);
67 | return cssFont;
68 | }
69 |
70 | static breakpoint(inputBreakpoint: string) {
71 | const cssBreakpoint = cssVar(`--breakpoints-${inputBreakpoint}`);
72 | return parseInt((cssBreakpoint as string).slice(0, -2));
73 | }
74 |
75 | static lineHeight(inputLineHeight: string) {
76 | const cssLineHeight = cssVar(`--lineHeights-${inputLineHeight}`);
77 | return remToPx(cssLineHeight);
78 | }
79 |
80 | static fontWeight(inputFontWeight: string) {
81 | const cssFontWeight = cssVar(`--fontWeights-${inputFontWeight}`);
82 | return cssFontWeight;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/lib/Utils/types.ts:
--------------------------------------------------------------------------------
1 | export type Modify = Omit & R;
2 |
3 | export type ModifyDeep> = {
4 | [K in keyof A]: B[K] extends never
5 | ? A[K]
6 | : B[K] extends AnyObject
7 | ? ModifyDeep
8 | : B[K];
9 | } & (A extends AnyObject ? Omit : A);
10 |
11 | /** Makes each property optional and turns each leaf property into any, allowing for type overrides by narrowing any. */
12 | export type DeepPartialAny = {
13 | [P in keyof T]?: T[P] extends AnyObject ? DeepPartialAny : any;
14 | };
15 |
16 | export type AnyObject = Record;
17 |
18 | export type UnionToIntersection = (
19 | U extends any ? (k: U) => void : never
20 | ) extends (k: infer I) => void
21 | ? I
22 | : never;
23 |
24 | export type Writeable = { -readonly [P in keyof T]: T[P] };
25 |
26 | export type DeepWriteable = {
27 | -readonly [P in keyof T]: DeepWriteable;
28 | };
29 |
30 | export type AsProp = {
31 | as?: C;
32 | };
33 |
34 | export type PropsToOmit = keyof (AsProp & P);
35 |
36 | // this type is used for when the component contains subcomponents (e.g. Checkbox.Group)
37 | export type MasterComponent<
38 | T,
39 | P = Record,
40 | V = Record
41 | > = React.ForwardRefExoticComponent<
42 | React.PropsWithoutRef & React.RefAttributes
43 | > &
44 | V;
45 |
46 | // reusable type utility for component props
47 | export type PolymorphicComponentProp<
48 | C extends React.ElementType,
49 | Props = Record
50 | > = React.PropsWithChildren> &
51 | Omit, PropsToOmit>;
52 |
53 | // same utility for component props with refs
54 | export type PolymorphicComponentPropWithRef<
55 | C extends React.ElementType,
56 | Props = Record
57 | > = PolymorphicComponentProp & { ref?: PolymorphicRef };
58 |
59 | // This is the type for the "ref" only
60 | export type PolymorphicRef =
61 | React.ComponentPropsWithRef['ref'];
62 |
--------------------------------------------------------------------------------
/src/lib/Utils/utils.test.tsx:
--------------------------------------------------------------------------------
1 | import { uuid } from './random';
2 | import { createPalette } from './color';
3 | import { __DEV__ } from './env';
4 |
5 | describe('utils/color', () => {
6 | it('generatePalette', () => {
7 | expect(createPalette({ primary: '#228BE6' })).toStrictEqual({
8 | primary: '#228BE6',
9 | 'primary-darken-1': '#187dd4',
10 | 'primary-darken-2': '#1673c4',
11 | 'primary-darken-3': '#146ab4',
12 | 'primary-darken-4': '#0e4778',
13 | 'primary-lighten-1': 'rgba(34,139,230,0.85)',
14 | 'primary-lighten-2': 'rgba(34,139,230,0.75)',
15 | 'primary-lighten-3': 'rgba(34,139,230,0.5)',
16 | 'primary-lighten-4': 'rgba(34,139,230,0.35)',
17 | 'primary-lighten-5': 'rgba(34,139,230,0.22)',
18 | 'primary-lighten-6': 'rgba(34,139,230,0.15)',
19 | 'primary-lighten-7': 'rgba(34,139,230,0.1)',
20 | 'primary-lighten-8': 'rgba(34,139,230,0.01)',
21 | 'primary-readable': '#fff',
22 | });
23 | });
24 | });
25 | describe('utils/random', () => {
26 | it('can use prefix', () => {
27 | expect(uuid('prefix')).toContain('prefix-');
28 | });
29 | });
30 | describe('utils/env', () => {
31 | it('returns true if process.NODE_ENV is development', () => {
32 | expect(__DEV__).toBe(process.env.NODE_ENV === 'development');
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "incremental": true,
4 | "target": "es2017",
5 | "outDir": "build/main",
6 | "rootDirs": ["src"],
7 | "moduleResolution": "node",
8 | "module": "commonjs",
9 | "declaration": true,
10 | "jsx": "react",
11 | "inlineSourceMap": true,
12 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
13 | "resolveJsonModule": true /* Include modules imported with .json extension. */,
14 |
15 | "strict": true /* Enable all strict type-checking options. */,
16 |
17 | /* Strict Type-Checking Options */
18 | // "noImplicitAny": true,
19 | // "strictNullChecks": true /* Enable strict null checks. */,
20 | // "strictFunctionTypes": true /* Enable strict checking of function types. */,
21 | // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
22 | // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
23 | // "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
24 |
25 | /* Additional Checks */
26 | "noUnusedLocals": false /* Report errors on unused locals. */,
27 | "noUnusedParameters": true /* Report errors on unused parameters. */,
28 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
29 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
30 |
31 | /* Debugging Options */
32 | "traceResolution": false /* Report module resolution log messages. */,
33 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */,
34 | "listFiles": false /* Print names of files part of the compilation. */,
35 | "pretty": true /* Stylize errors and messages using color and context. */,
36 |
37 | /* Experimental Options */
38 | // "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
39 | // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
40 |
41 | "lib": ["es2017", "dom"],
42 | "types": ["node", "jest"],
43 | "typeRoots": ["node_modules/@types", "src/types"],
44 | "baseUrl": "./",
45 | "paths": {
46 | "@lib/*": ["./src/lib/*"]
47 | }
48 | },
49 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.test.tsx", "src/**/*.stories.tsx"],
50 | "exclude": ["node_modules/**", "cypress", "cypress.config.ts", "src/**/*.cy.tsx"],
51 | "compileOnSave": false
52 | }
53 |
--------------------------------------------------------------------------------
/tsconfig.module.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "target": "esnext",
5 | "outDir": "build/module",
6 | "module": "esnext"
7 | },
8 | "exclude": ["node_modules/**"]
9 | }
10 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 | import path from 'path';
4 |
5 | export default defineConfig({
6 | resolve: {
7 | alias: {
8 | '@lib': path.resolve(__dirname, './src/lib'),
9 | },
10 | },
11 | plugins: [react()],
12 | });
13 |
--------------------------------------------------------------------------------