├── .eslintrc.base.js
├── .eslintrc.js
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── apps
├── boilerplate-nextjs
│ ├── .gitignore
│ ├── README.md
│ ├── babel.config.js
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── api
│ │ │ └── hello.ts
│ │ └── index.tsx
│ ├── public
│ │ └── favicon.ico
│ ├── src
│ │ ├── components
│ │ │ ├── Header.tsx
│ │ │ ├── Layout.tsx
│ │ │ ├── Meta.tsx
│ │ │ ├── about.mdx
│ │ │ ├── about.tsx
│ │ │ ├── example-components.tsx
│ │ │ ├── footer.tsx
│ │ │ ├── intro.tsx
│ │ │ ├── page.tsx
│ │ │ └── theme-switcher.tsx
│ │ ├── design-system.ts
│ │ ├── images
│ │ │ └── logo.svg
│ │ └── providers
│ │ │ ├── index.tsx
│ │ │ ├── mdx-provider.tsx
│ │ │ └── prism-theme.ts
│ └── tsconfig.json
└── example-parcel
│ ├── . parcelrc
│ ├── .babelrc
│ ├── .gitignore
│ ├── README.md
│ ├── custom.d.ts
│ ├── emotion.d.ts
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── manifest.json
│ └── robots.txt
│ ├── src
│ ├── App.tsx
│ ├── components
│ │ ├── Header.tsx
│ │ ├── Layout.tsx
│ │ ├── footer.tsx
│ │ ├── page.tsx
│ │ └── theme-switcher.tsx
│ ├── design-system.ts
│ ├── images
│ │ └── logo.svg
│ ├── index.tsx
│ ├── pages
│ │ └── home
│ │ │ ├── about.tsx
│ │ │ ├── example-components.tsx
│ │ │ ├── home.tsx
│ │ │ └── intro.tsx
│ ├── providers
│ │ └── index.tsx
│ └── setupTests.ts
│ └── tsconfig.json
├── babel.config.js
├── package.json
├── packages
└── design-system
│ ├── .gitignore
│ ├── .storybook
│ ├── main.js
│ └── preview.js
│ ├── README.md
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ ├── components
│ │ ├── Box
│ │ │ ├── Box.stories.tsx
│ │ │ ├── Box.tsx
│ │ │ └── index.ts
│ │ ├── Button
│ │ │ ├── Button.stories.tsx
│ │ │ ├── Button.styles.ts
│ │ │ ├── Button.tsx
│ │ │ └── index.ts
│ │ ├── CSSReset
│ │ │ ├── CSSReset.tsx
│ │ │ ├── index.ts
│ │ │ └── reset.tsx
│ │ ├── Callout
│ │ │ ├── Callout.stories.tsx
│ │ │ ├── Callout.tsx
│ │ │ └── index.ts
│ │ ├── Text
│ │ │ ├── Text.stories.tsx
│ │ │ ├── Text.styles.ts
│ │ │ ├── Text.tsx
│ │ │ └── index.ts
│ │ ├── ThemeProvider
│ │ │ ├── ThemeProvider.tsx
│ │ │ ├── global-styles.ts
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── core
│ │ ├── style-presets.ts
│ │ ├── theme.ts
│ │ └── types.ts
│ ├── examples
│ │ └── Homepage
│ │ │ ├── Intro.tsx
│ │ │ ├── Layout.tsx
│ │ │ ├── Page.tsx
│ │ │ ├── Principles.tsx
│ │ │ └── ThemeSwitcher.tsx
│ ├── hooks
│ │ └── useToggleTheme.ts
│ ├── index.ts
│ ├── storybook-helpers
│ │ ├── StorybookLayout.tsx
│ │ └── args.ts
│ ├── tokens
│ │ ├── breakpoints.ts
│ │ ├── colors.ts
│ │ ├── radii.ts
│ │ ├── space.ts
│ │ └── typography.ts
│ ├── types
│ │ └── index.ts
│ └── util
│ │ ├── __tests__
│ │ ├── create-theme.test.ts
│ │ └── object.test.ts
│ │ ├── create-theme.ts
│ │ ├── helpers.ts
│ │ └── object.ts
│ ├── tsconfig.jest.json
│ └── tsconfig.json
├── tsconfig.json
└── yarn.lock
/.eslintrc.base.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | es6: true,
6 | },
7 | ignorePatterns: ['node_modules/*', '.eslintrc.js'],
8 | parser: '@typescript-eslint/parser',
9 | parserOptions: {
10 | ecmaFeatures: {
11 | jsx: true,
12 | globalReturn: false,
13 | },
14 | ecmaVersion: 2020,
15 | },
16 | settings: {
17 | react: {
18 | version: 'detect',
19 | },
20 | 'import/resolver': {
21 | typescript: {},
22 | },
23 | },
24 | extends: [
25 | 'plugin:import/errors',
26 | 'plugin:import/warnings',
27 | 'plugin:import/typescript',
28 | 'plugin:@typescript-eslint/recommended',
29 | 'plugin:regexp/recommended',
30 | 'plugin:prettier/recommended',
31 | ],
32 | plugins: ['@emotion', 'jest'],
33 | globals: {
34 | context: 'readonly',
35 | cy: 'readonly',
36 | assert: 'readonly',
37 | Cypress: 'readonly',
38 | },
39 | rules: {
40 | 'linebreak-style': ['error', 'unix'],
41 | 'no-empty-function': 'off',
42 | '@typescript-eslint/no-empty-function': [
43 | 'error',
44 | { allow: ['private-constructors'] },
45 | ],
46 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
47 | 'import/default': 'off',
48 | 'import/no-named-as-default-member': 'off',
49 | 'import/no-named-as-default': 'off',
50 | 'import/order': [
51 | 'error',
52 | {
53 | groups: [
54 | 'builtin',
55 | 'external',
56 | 'internal',
57 | 'parent',
58 | 'sibling',
59 | 'index',
60 | 'object',
61 | ],
62 | alphabetize: { order: 'asc', caseInsensitive: true },
63 | },
64 | ],
65 | },
66 | overrides: [
67 | {
68 | // For performance run jest/recommended on test files, not regular code
69 | files: ['**/?(*.)+(test|spec).{js,jsx,ts,tsx}'],
70 | extends: ['plugin:jest/recommended'],
71 | rules: {
72 | '@typescript-eslint/no-non-null-assertion': 'off',
73 | '@typescript-eslint/no-object-literal-type-assertion': 'off',
74 | '@typescript-eslint/no-empty-function': 'off',
75 | },
76 | },
77 | {
78 | files: ['*.js'],
79 | rules: {
80 | '@typescript-eslint/ban-ts-comment': 'off',
81 | '@typescript-eslint/no-explicit-any': 'off',
82 | '@typescript-eslint/no-var-requires': 'off',
83 | '@typescript-eslint/explicit-module-boundary-types': 'off',
84 | 'import/order': 'off',
85 | },
86 | },
87 | ],
88 | };
89 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: [
4 | './.eslintrc.base.js',
5 | 'plugin:react/recommended',
6 | 'plugin:react-hooks/recommended',
7 | 'plugin:jsx-a11y/recommended',
8 | ],
9 | // By loading testing-library as a plugin, we can only enable it
10 | // on test files via overrides.
11 | ignorePatterns: ['**/dist/*', '**/.storybook/*'],
12 | plugins: ['testing-library'],
13 | // plugins: ['testing-library', 'storybook'],
14 | env: {
15 | browser: true,
16 | es6: true,
17 | node: true,
18 | },
19 | rules: {
20 | 'react/prop-types': 'off',
21 | 'react/react-in-jsx-scope': 'off',
22 | 'jsx-a11y/anchor-is-valid': 'off',
23 | },
24 | overrides: [
25 | {
26 | // For performance run jest/recommended on test files, not regular code
27 | files: ['**/__tests__/**/*.{ts,tsx}'],
28 | extends: ['plugin:testing-library/react'],
29 | },
30 | // {
31 | // // For performance run storybook/recommended on test files, not regular code
32 | // files: ['**/*.stories.{ts,tsx,mdx}'],
33 | // extends: ['plugin:storybook/recommended'],
34 | // },
35 | ],
36 | };
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .parcel-cache
2 |
3 | .vscode
4 |
5 | /node_modules
6 |
7 | yarn-error.log
8 |
9 | .DS_Store
10 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .yarn
2 | **/.next/**
3 | **/dist/**
4 | **/tmp/**
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": true,
4 | "tabWidth": 2,
5 | "bracketSpacing": true,
6 | "trailingComma": "es5",
7 | "bracketSameLine": true,
8 | "useTabs": false,
9 | "endOfLine": "auto",
10 | "overrides": [
11 | {
12 | "files": "*.md",
13 | "options": {
14 | "singleQuote": false,
15 | "quoteProps": "preserve"
16 | }
17 | }
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Dinesh Pandiyan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Design System Boilerplate
2 |
3 | A highly scalable, performance focused design system boilerplate setup with **configurable tokens**, **smart theming**, **strong types** and auto-generated css variables.
4 |
5 | > You can **server render themes** with zero runtime concerns. [This website](https://design-system-boilerplate.netlify.app) is server rendered with light theme. Try reloading the page after disabling JavaScript and you'll see the server rendered theme in action.
6 |
7 | ## How does it work?
8 |
9 | - Themes are statically compiled (no runtime theming).
10 | - Components are styled using [Emotion](https://emotion.sh/docs/introduction).
11 | - Tokens are based on [System UI Theme Specification](https://system-ui.com/theme/).
12 | - Components are built with [Styled System](https://styled-system.com/) component API for rapid UI development.
13 | - Strong types with full token autocomplete support in component APIs
14 |
15 | ## Principles
16 |
17 | The design system is built with a set of constraints that allows us to statically compile theme definitions and build flexibile APIs.
18 |
19 | - Predefine all your themes. i.e _no runtime theme creation_. This allows us to statically define themes using css variables and create strongly typed theme tokens. Strong types help with full autocomplete support in component APIs. Eg.
20 |
21 | - **Box** component `backgroundColor` prop autocompletes with available color tokens - `primary`, `secondary`, etc.
22 | - **Box** component `padding` prop autocompletes with available spacing tokens - `small`, `large`, etc.
23 |
24 | - Token groups are identified and based on [System UI Theme Specification](https://system-ui.com/theme/). Eg. `colors`, `space`, `fontSizes`, etc.
25 |
26 | - Keep your token groups flat. Don't nest your tokens within token groups. Eg. `colors.primary` is allowed. `colors.primary.success` is not allowed.
27 |
28 | - All themes should have the same set of tokens. Eg. _dark_ theme cannot have a token that's not available in _light_ theme.
29 |
30 | - Theming is setup with CSS custom properties (css variables). This makes it easy to switch or server render themes without runtime concerns. Eg. `token.colors.primary` has the same css variable across themes and makes it easy to statically define styles instead of defining the styles during runtime based on theme. `background: ${tokens.colors.primary}` instead of `background: ${(prop) => prop.theme.colors.primary}`.
31 |
32 | ## How does theming work?
33 |
34 | - Theme definitions are automatically converted to css variables using the `createTheme` utility.
35 | - The generated css variables are theme-name scoped in `ThemeProvider`.
36 |
37 | This is how the generated css variables are inserted into the global stylesheet —
38 |
39 | ```css
40 | body[data-theme='light'] {
41 | --theme-colors-primary: blue;
42 | --theme-colors-secondary: lightblue;
43 | }
44 | body[data-theme='dark'] {
45 | --theme-colors-primary: green;
46 | --theme-colors-secondary: lightgreen;
47 | }
48 | ```
49 |
50 | So when you use a token in your component —
51 |
52 | ```jsx
53 | const Example = () => {
54 | return (
55 |
Hello World
56 | );
57 | };
58 | ```
59 |
60 | This is what is used as token value —
61 |
62 | ```jsx
63 | const Example = () => {
64 | return (
65 |
66 | Hello World
67 |
68 | );
69 | };
70 | ```
71 |
72 | And the css variable `--theme-colors-primary` value is scoped based on the data attribute in body element — ``
73 |
74 | ## Server rendering themes
75 |
76 | Theming is completely css driven. All themes are statically defined using tokens which are then converted to css variables. Current theme is decided based on a html attribute on body element. Eg. ``. So server rendering themes is just a matter of rendering the right theme name as body attribute.
77 |
78 | ## Example theme usage
79 |
80 | ### Using tokens
81 |
82 | All themes use the same css variable names as token values. So you can define the styles statically without needing runtime theme prop.
83 |
84 | ```jsx
85 | import { tokens } from '@unpublished/design-system';
86 |
87 | const Example = () => {
88 | return (
89 | Hello World
90 | );
91 | };
92 | ```
93 |
94 | ### Getting current theme name
95 |
96 | Getting current theme is as simple as querying the DOM attribute. You don't need Context or fancy hooks to provide you the value.
97 |
98 | ```js
99 | const themeName = document.body.getAttribute('data-theme');
100 | ```
101 |
102 | _Note: You can use a context based hook if you need — `useTheme()`._
103 |
104 | ### Switching theme
105 |
106 | Switching theme is as simple as setting the theme name on body element. Since themes are completely css driven, themes can be changed without re-rendering the whole tree.
107 |
108 | ```js
109 | document.body.setAttribute('data-theme', 'dark');
110 | ```
111 |
112 | _Note: You can also re-render the whole tree on theme change using `useTheme()` hook if you have to._
113 |
114 | ## License
115 |
116 | MIT © [Dinesh Pandiyan](https://github.com/flexdinesh)
117 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | ```
12 |
13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14 |
15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
16 |
17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
18 |
19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
20 |
21 | ## Learn More
22 |
23 | To learn more about Next.js, take a look at the following resources:
24 |
25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
27 |
28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
29 |
30 | ## Deploy on Vercel
31 |
32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
33 |
34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
35 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | 'next/babel',
5 | {
6 | 'preset-react': {
7 | runtime: 'automatic',
8 | importSource: '@emotion/react',
9 | },
10 | },
11 | ],
12 | ],
13 | plugins: ['@emotion/babel-plugin'],
14 | };
15 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 |
3 | const withMDX = require('@next/mdx')({
4 | extension: /\.mdx?$/,
5 | options: {
6 | remarkPlugins: [],
7 | rehypePlugins: [],
8 | // If you use `MDXProvider`, uncomment the following line.
9 | providerImportSource: '@mdx-js/react',
10 | },
11 | });
12 |
13 | module.exports = withMDX({
14 | reactStrictMode: true,
15 | pageExtensions: ['ts', 'tsx'],
16 | });
17 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ds-boilerplate/nextjs-example",
3 | "private": true,
4 | "version": "0.0.1",
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "build:static": "next build && next export",
9 | "clean": "rimraf --no-glob ./out .next ./tsconfig.tsbuildinfo ./node_modules/.cache ",
10 | "start": "next start",
11 | "lint": "next lint"
12 | },
13 | "dependencies": {
14 | "@emotion/css": "^11.7.1",
15 | "@emotion/react": "^11.7.1",
16 | "@emotion/server": "^11.4.0",
17 | "@mdx-js/loader": "^2.0.0",
18 | "@mdx-js/react": "^2.0.0",
19 | "@next/mdx": "^12.0.10",
20 | "@unpublished/design-system": "0.0.1",
21 | "next": "12.0.7",
22 | "prism-react-renderer": "^1.3.1",
23 | "react": "^17.0.2",
24 | "react-dom": "^17.0.2"
25 | },
26 | "devDependencies": {
27 | "@emotion/babel-plugin": "11.7.2",
28 | "@types/node": "^17.0.10",
29 | "@types/react": "17.0.38",
30 | "eslint": "8.5.0",
31 | "eslint-config-next": "12.0.7",
32 | "typescript": "4.5.4",
33 | "typescript-styled-plugin": "^0.18.2"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import type { AppProps } from 'next/app';
2 |
3 | function MyApp({ Component, pageProps }: AppProps) {
4 | return ;
5 | }
6 |
7 | export default MyApp;
8 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { cache } from '@emotion/css';
2 | import createEmotionServer from '@emotion/server/create-instance';
3 | import Document, {
4 | Html,
5 | Head,
6 | Main,
7 | NextScript,
8 | DocumentContext,
9 | } from 'next/document';
10 | import * as React from 'react';
11 |
12 | const renderStatic = async (html: string | undefined) => {
13 | if (html === undefined) {
14 | throw new Error('did you forget to return html from renderToString?');
15 | }
16 | const { extractCritical } = createEmotionServer(cache);
17 | const { ids, css } = extractCritical(html);
18 | return { html, ids, css };
19 | };
20 |
21 | export default class AppDocument extends Document {
22 | static async getInitialProps(ctx: DocumentContext) {
23 | const page = await ctx.renderPage();
24 | const { css, ids } = await renderStatic(page.html);
25 | const initialProps = await Document.getInitialProps(ctx);
26 |
27 | return {
28 | ...initialProps,
29 | styles: (
30 |
31 | {initialProps.styles}
32 |
36 |
37 | ),
38 | };
39 | }
40 |
41 | render() {
42 | return (
43 |
44 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 | import type { NextApiRequest, NextApiResponse } from 'next';
3 |
4 | type Data = {
5 | name: string;
6 | };
7 |
8 | export default function handler(
9 | req: NextApiRequest,
10 | res: NextApiResponse
11 | ) {
12 | res.status(200).json({ name: 'John Doe' });
13 | }
14 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/pages/index.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 | import type { NextPage } from 'next';
4 | import { About } from '../src/components/about';
5 | import { ExampleComponents } from '../src/components/example-components';
6 | import { Footer } from '../src/components/footer';
7 | import { Header } from '../src/components/header';
8 | import { Intro } from '../src/components/intro';
9 | import { Layout } from '../src/components/layout';
10 | import { Page } from '../src/components/page';
11 | import { Box, tokens } from '../src/design-system';
12 |
13 | const styles = {
14 | homePage: css`
15 | min-height: 100vh;
16 | `,
17 | main: css`
18 | height: 100%;
19 | padding: 0 2rem;
20 | margin: 4rem 0 2rem;
21 | `,
22 | };
23 |
24 | const GHIcon = () => {
25 | return (
26 |
32 |
33 |
36 |
37 |
38 | );
39 | };
40 |
41 | const Home: NextPage = () => {
42 | return (
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default Home;
72 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flexdinesh/design-system-boilerplate/b7c444cb7743cf7ac53d4fc6703af8330ec7a6dd/apps/boilerplate-nextjs/public/favicon.ico
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 | import { Box, mediaQueries } from '../design-system';
4 | import { ThemeSwitcher } from './theme-switcher';
5 |
6 | export const styles = {
7 | header: css`
8 | width: 100%;
9 | position: fixed;
10 | left: 0;
11 | top: 0;
12 | height: 4rem;
13 | display: flex;
14 | gap: 8px;
15 | align-items: center;
16 | justify-content: flex-end;
17 | padding: 0 1rem;
18 | backdrop-filter: blur(1px);
19 | ${mediaQueries.medium} {
20 | backdrop-filter: none;
21 | }
22 | `,
23 | logo: css`
24 | pointer-events: none;
25 | justify-self: center;
26 | `,
27 | };
28 |
29 | export const Header = () => {
30 | return (
31 |
32 |
33 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { Global } from '@emotion/react';
3 | import React from 'react';
4 | import Meta from '../components/meta';
5 | import { CSSReset, tokens } from '../design-system';
6 | import Providers from '../providers';
7 |
8 | export const Layout: React.FC = ({ children }) => {
9 | return (
10 |
11 |
12 |
13 |
21 | {children}
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/components/Meta.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 |
3 | const Meta = () => {
4 | return (
5 |
6 | Design System Boilerplate - Next.js Example
7 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default Meta;
17 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/components/about.mdx:
--------------------------------------------------------------------------------
1 | A highly scalable, performance focused design system boilerplate setup with **configurable tokens**, **smart theming**, **strong types** and auto-generated css variables.
2 |
3 | > You can **server render themes** with zero runtime concerns. This page is server rendered with light theme. Try reloading the page after disabling JavaScript and you'll see the server rendered theme in action.
4 |
5 | ## How does it work?
6 |
7 | - Themes are statically compiled (no runtime theming).
8 | - Components are styled using [Emotion](https://emotion.sh/docs/introduction).
9 | - Tokens are based on [System UI Theme Specification](https://system-ui.com/theme/).
10 | - Components are built with [Styled System](https://styled-system.com/) component API for rapid UI development.
11 | - Strong types with full token autocomplete support in component APIs
12 |
13 | ## Principles
14 |
15 | The design system is built with a set of constraints that allows us to statically compile theme definitions and build flexibile APIs.
16 |
17 | - Predefine all your themes. i.e _no runtime theme creation_. This allows us to statically define themes using css variables and create strongly typed theme tokens. Strong types help with full autocomplete support in component APIs. Eg.
18 |
19 | - **Box** component `backgroundColor` prop autocompletes with available color tokens - `primary`, `secondary`, etc.
20 | - **Box** component `padding` prop autocompletes with available spacing tokens - `small`, `large`, etc.
21 |
22 | - Token groups are identified and based on [System UI Theme Specification](https://system-ui.com/theme/). Eg. `colors`, `space`, `fontSizes`, etc.
23 |
24 | - Keep your token groups flat. Don't nest your tokens within token groups. Eg. `colors.primary` is allowed. `colors.primary.success` is not allowed.
25 |
26 | - All themes should have the same set of tokens. Eg. _dark_ theme cannot have a token that's not available in _light_ theme.
27 |
28 | - Theming is setup with CSS custom properties (css variables). This makes it easy to switch or server render themes without runtime concerns. Eg. `token.colors.primary` has the same css variable across themes and makes it easy to statically define styles instead of defining the styles during runtime based on theme. `background: ${tokens.colors.primary}` instead of `background: ${(prop) => prop.theme.colors.primary}`.
29 |
30 | ## How does theming work?
31 |
32 | - Theme definitions are automatically converted to css variables using the `createTheme` utility.
33 | - The generated css variables are theme-name scoped in `ThemeProvider`.
34 |
35 | This is how the generated css variables are inserted into the global stylesheet —
36 |
37 | ```css
38 | body[data-theme='light'] {
39 | --theme-colors-primary: blue;
40 | --theme-colors-secondary: lightblue;
41 | }
42 | body[data-theme='dark'] {
43 | --theme-colors-primary: green;
44 | --theme-colors-secondary: lightgreen;
45 | }
46 | ```
47 |
48 | So when you use a token in your component —
49 |
50 | ```jsx
51 | const Example = () => {
52 | return (
53 | Hello World
54 | );
55 | };
56 | ```
57 |
58 | This is what is used as token value —
59 |
60 | ```jsx
61 | const Example = () => {
62 | return (
63 |
64 | Hello World
65 |
66 | );
67 | };
68 | ```
69 |
70 | And the css variable `--theme-colors-primary` value is scoped based on the data attribute in body element — ``
71 |
72 | ## Server rendering themes
73 |
74 | Theming is completely css driven. All themes are statically defined using tokens which are then converted to css variables. Current theme is decided based on a html attribute on body element. Eg. ``. So server rendering themes is just a matter of rendering the right theme name as body attribute.
75 |
76 | ## Example theme usage
77 |
78 | ### Using tokens
79 |
80 | All themes use the same css variable names as token values. So you can define the styles statically without needing runtime theme prop.
81 |
82 | ```jsx
83 | import { tokens } from '@unpublished/design-system';
84 |
85 | const Example = () => {
86 | return (
87 | Hello World
88 | );
89 | };
90 | ```
91 |
92 | ### Getting current theme name
93 |
94 | Getting current theme is as simple as querying the DOM attribute. You don't need Context or fancy hooks to provide you the value.
95 |
96 | ```js
97 | const themeName = document.body.getAttribute('data-theme');
98 | ```
99 |
100 | _Note: You can use a context based hook if you need — `useTheme()`._
101 |
102 | ### Switching theme
103 |
104 | Switching theme is as simple as setting the theme name on body element. Since themes are completely css driven, themes can be changed without re-rendering the whole tree.
105 |
106 | ```js
107 | document.body.setAttribute('data-theme', 'dark');
108 | ```
109 |
110 | _Note: You can also re-render the whole tree on theme change using `useTheme()` hook if you have to._
111 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/components/about.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 | import { Box } from '../design-system';
4 | import AboutMDX from './about.mdx';
5 |
6 | export const styles = {
7 | about: css`
8 | margin-top: 3rem;
9 | `,
10 | };
11 |
12 | export const About = () => {
13 | return (
14 |
15 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/components/example-components.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 | import { Box, Text, Button, Callout, tokens } from '../design-system';
4 |
5 | export const styles = {
6 | exampleComponents: css`
7 | margin: 2rem 0 0;
8 | `,
9 | };
10 |
11 | export const ExampleComponents = () => {
12 | return (
13 |
14 | Example Components
15 |
16 | Two example components built with tokens to demonstrate theming.
17 |
18 | Button
19 |
27 | Primary
28 | Secondary
29 |
30 | Callout
31 |
39 |
40 |
41 | Primary variant. You can write anything here to
42 | bring extra attention to your users.
43 |
44 |
45 |
46 |
47 | Secondary variant. You can write anything here to
48 | bring extra attention to your users.
49 |
50 |
51 |
52 |
53 | Ghost variant. You can write anything here to bring
54 | extra attention to your users.
55 |
56 |
57 |
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/components/footer.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 | import { Box, Text, tokens } from '../design-system';
4 |
5 | export const styles = {
6 | footer: css`
7 | box-shadow: 0 1px 0 0 ${tokens.colors.border} inset;
8 | height: 5rem;
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | padding: 0 1rem;
13 | font-size: ${tokens.fontSizes.body};
14 | background: ${tokens.colors.codeBg};
15 | color: ${tokens.colors.codeText};
16 | `,
17 | };
18 |
19 | export const Footer = () => {
20 | return (
21 |
22 |
23 | Made by{' '}
24 |
46 | Dinesh
47 |
48 |
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/components/intro.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 | import { Box, Text, tokens } from '../design-system';
4 |
5 | export const styles = {
6 | intro: css`
7 | display: flex;
8 | flex-direction: column;
9 | align-items: center;
10 | justify-content: center;
11 | padding: 0 0.5rem;
12 | `,
13 | title: css`
14 | width: 100%;
15 | line-height: ${tokens.lineHeights.base};
16 | display: flex;
17 | flex-direction: column;
18 | justify-content: center;
19 | border: 1px solid;
20 | letter-spacing: -0.05em;
21 | padding: 1rem;
22 | border-color: ${tokens.colors.primaryBase};
23 | box-shadow: ${tokens.colors.primaryBase} 4px 4px 0 0;
24 | `,
25 | };
26 |
27 | export const Intro = () => {
28 | return (
29 |
30 |
31 | A Design System Template
32 |
42 | That Scales.
43 |
44 |
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/components/page.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 |
4 | const styles = {
5 | page: css`
6 | max-width: 960px;
7 | margin-left: auto;
8 | margin-right: auto;
9 | padding-left: 16px;
10 | padding-right: 16px;
11 | `,
12 | };
13 |
14 | export const Page: React.FC = ({ children }) => {
15 | return {children}
;
16 | };
17 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/components/theme-switcher.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import type { FC } from 'react';
3 | import { Button, useToggleTheme } from '../design-system';
4 |
5 | export const ThemeSwitcher: FC = () => {
6 | const { theme, toggleTheme } = useToggleTheme();
7 |
8 | return (
9 |
10 | {theme}
11 |
12 | );
13 | };
14 |
15 | if (process.env.NODE_ENV !== 'production') {
16 | ThemeSwitcher.displayName = 'ThemeSwitcher';
17 | }
18 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/design-system.ts:
--------------------------------------------------------------------------------
1 | /*
2 | Importing the design system imports from a local file because I keep changing
3 | how the local package is linked and it's easier to change all the import statements in one file
4 | */
5 | import {
6 | CSSReset,
7 | ThemeProvider,
8 | Box,
9 | Button,
10 | Text,
11 | Callout,
12 | themes,
13 | tokens,
14 | useToggleTheme,
15 | mediaQueries,
16 | } from '@unpublished/design-system';
17 |
18 | import type { Tokens } from '@unpublished/design-system';
19 |
20 | export {
21 | CSSReset,
22 | ThemeProvider,
23 | useToggleTheme,
24 | Box,
25 | Button,
26 | Text,
27 | Callout,
28 | themes,
29 | tokens,
30 | mediaQueries,
31 | };
32 |
33 | export type { Tokens };
34 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/providers/index.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider, themes } from '../design-system';
2 | import { MDXProvider } from './mdx-provider';
3 |
4 | const Providers: React.FC = ({ children }) => {
5 | return (
6 |
7 | {children}
8 |
9 | );
10 | };
11 |
12 | export default Providers;
13 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/providers/mdx-provider.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/ban-ts-comment */
2 | /** @jsxImportSource @emotion/react */
3 | import { MDXProvider as MDXProviderImported } from '@mdx-js/react';
4 | import Highlight, { defaultProps } from 'prism-react-renderer';
5 | import React from 'react';
6 | import { Callout, Text, tokens } from '../design-system';
7 | import { theme } from './prism-theme';
8 |
9 | const CodeHighlight: React.FC<{ className: string; children: string }> = ({
10 | className,
11 | children,
12 | }) => {
13 | const language = className?.replace(/language-/, '') as React.ComponentProps<
14 | typeof Highlight
15 | >['language'];
16 |
17 | if (!language) {
18 | return (
19 |
33 | {children?.trim()}
34 |
35 | );
36 | }
37 |
38 | children = typeof children === 'string' ? children.trim() : children;
39 |
40 | return (
41 |
46 | {({
47 | className,
48 | style,
49 | tokens: codeTokens,
50 | getLineProps,
51 | getTokenProps,
52 | }) => {
53 | return (
54 |
78 | {codeTokens.map((line, i) => (
79 | // eslint-disable-next-line react/jsx-key
80 |
81 | {line.map((token, key) => (
82 | // eslint-disable-next-line react/jsx-key
83 |
84 | ))}
85 |
86 | ))}
87 |
88 | );
89 | }}
90 |
91 | );
92 | };
93 |
94 | const H1: React.FC = (props) => (
95 |
96 | {props.children}
97 |
98 | );
99 |
100 | const H2: React.FC = (props) => (
101 |
102 | {props.children}
103 |
104 | );
105 |
106 | const H3: React.FC = (props) => (
107 |
108 | {props.children}
109 |
110 | );
111 |
112 | const H4: React.FC = (props) => (
113 |
114 | {props.children}
115 |
116 | );
117 |
118 | const P: React.FC = (props) => (
119 |
120 | {props.children}
121 |
122 | );
123 |
124 | const Blockquote: React.FC = (props) => (
125 | // @ts-ignore
126 |
127 | {props.children}
128 |
129 | );
130 |
131 | const Li: React.FC = (props) => (
132 |
139 | {props.children}
140 |
141 | );
142 |
143 | const components = {
144 | h1: H1,
145 | h2: H2,
146 | h3: H3,
147 | h4: H4,
148 | p: P,
149 | li: Li,
150 | code: CodeHighlight,
151 | blockquote: Blockquote,
152 | };
153 |
154 | export const MDXProvider: React.FC = ({ children }) => {
155 | return (
156 | // @ts-ignore
157 |
158 | {children}
159 |
160 | );
161 | };
162 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/src/providers/prism-theme.ts:
--------------------------------------------------------------------------------
1 | import { tokens } from '../design-system';
2 |
3 | export const theme = {
4 | plain: {
5 | color: tokens.colors.textHeading,
6 | backgroundColor: '#f6f7f9',
7 | },
8 | styles: [
9 | {
10 | types: ['comment', 'prolog', 'doctype', 'cdata'],
11 | style: {
12 | color: tokens.colors.textBody,
13 | },
14 | },
15 | {
16 | types: ['namespace'],
17 | style: {
18 | opacity: 0.7,
19 | },
20 | },
21 | {
22 | types: ['string', 'attr-value'],
23 | style: {
24 | color: '#eC1337',
25 | },
26 | },
27 | {
28 | types: ['punctuation', 'operator'],
29 | style: {
30 | color: '#94A3B8',
31 | },
32 | },
33 | {
34 | types: [
35 | 'entity',
36 | 'url',
37 | 'symbol',
38 | 'number',
39 | 'boolean',
40 | 'variable',
41 | 'constant',
42 | 'property',
43 | 'regex',
44 | 'inserted',
45 | ],
46 | style: {
47 | color: '#04865d',
48 | },
49 | },
50 | {
51 | types: ['atrule', 'keyword', 'attr-name', 'selector'],
52 | style: {
53 | color: '#0080A8',
54 | },
55 | },
56 | {
57 | types: ['function', 'deleted', 'tag'],
58 | style: {
59 | color: tokens.colors.textHeading,
60 | },
61 | },
62 | {
63 | types: ['function-variable'],
64 | style: {
65 | color: '#7f54e0',
66 | },
67 | },
68 | {
69 | types: ['tag', 'selector', 'keyword'],
70 | style: {
71 | color: '#037dbb',
72 | },
73 | },
74 | ],
75 | };
76 |
--------------------------------------------------------------------------------
/apps/boilerplate-nextjs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "allowJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "incremental": true,
9 | "isolatedModules": true,
10 | "jsx": "preserve",
11 | "lib": ["dom", "dom.iterable", "esnext"],
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "noEmit": true,
15 | "resolveJsonModule": true,
16 | "skipLibCheck": true,
17 | "strict": true,
18 | "target": "esnext",
19 | "plugins": [
20 | {
21 | "name": "typescript-styled-plugin"
22 | }
23 | ],
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
26 | "exclude": ["node_modules", ".next"]
27 | }
28 |
--------------------------------------------------------------------------------
/apps/example-parcel/. parcelrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@parcel/config-default",
3 | "transformers": {
4 | "*.svg": ["...", "@parcel/transformer-svg-react"]
5 | }
6 | }
--------------------------------------------------------------------------------
/apps/example-parcel/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-react",
5 | { "runtime": "automatic", "importSource": "@emotion/react" }
6 | ]
7 | ],
8 | "plugins": ["@emotion/babel-plugin"]
9 | }
10 |
--------------------------------------------------------------------------------
/apps/example-parcel/.gitignore:
--------------------------------------------------------------------------------
1 | # build
2 | build
3 | dist
4 | .parcel-cache
5 |
6 | # dependencies
7 | node_modules
8 | /.pnp
9 | .pnp.js
10 |
11 | # testing
12 | /coverage
13 |
14 | # production
15 | /dist
16 |
17 | # misc
18 | .DS_Store
19 | .env.local
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 |
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
--------------------------------------------------------------------------------
/apps/example-parcel/README.md:
--------------------------------------------------------------------------------
1 | # Create React App Example
2 |
3 | Example app created with Create React App
--------------------------------------------------------------------------------
/apps/example-parcel/custom.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | import React = require('react');
3 | export const ReactComponent: React.FC>;
4 | const src: string;
5 | export default src;
6 | }
7 |
--------------------------------------------------------------------------------
/apps/example-parcel/emotion.d.ts:
--------------------------------------------------------------------------------
1 | import '@emotion/react';
2 | import type { Tokens } from './src/design-system';
3 |
4 | declare module '@emotion/react' {
5 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
6 | export interface Theme extends Tokens {}
7 | }
8 |
--------------------------------------------------------------------------------
/apps/example-parcel/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ds-boilerplate/parcel-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "parcel public/index.html --cache-dir .parcel-cache",
7 | "build": "parcel build public/index.html --cache-dir .parcel-cache",
8 | "clean": "rimraf --no-glob ./dist ./build ./.parcel-cache ./tsconfig.tsbuildinfo ./node_modules/.cache",
9 | "typecheck": "tsc --project ./tsconfig.json --noEmit"
10 | },
11 | "dependencies": {
12 | "@emotion/react": "^11.7.1",
13 | "@emotion/styled": "^11.6.0",
14 | "@mdx-js/react": "^2.0.0",
15 | "@testing-library/jest-dom": "^5.16.1",
16 | "@testing-library/react": "^12.1.2",
17 | "@testing-library/user-event": "^13.5.0",
18 | "@types/jest": "^27.0.3",
19 | "@types/node": "^17.0.10",
20 | "@types/react": "17.0.38",
21 | "@types/react-dom": "^17.0.11",
22 | "@unpublished/design-system": "0.0.1",
23 | "eslint-config-react-app": "^7.0.0",
24 | "react": "^17.0.2",
25 | "react-dom": "^17.0.2",
26 | "typescript": "4.5.4"
27 | },
28 | "devDependencies": {
29 | "@parcel/transformer-svg-react": "^2.3.1",
30 | "parcel": "^2.3.1"
31 | },
32 | "// browserslist comment": "// https://github.com/parcel-bundler/parcel/issues/7101#issuecomment-998580495",
33 | "browserslist": [
34 | "> 0.5%",
35 | "last 2 versions",
36 | "not dead",
37 | "not ios_saf < 13"
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/apps/example-parcel/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flexdinesh/design-system-boilerplate/b7c444cb7743cf7ac53d4fc6703af8330ec7a6dd/apps/example-parcel/public/favicon.ico
--------------------------------------------------------------------------------
/apps/example-parcel/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Design System Boilerplate
7 |
8 |
9 | You need to enable JavaScript to run this app.
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/apps/example-parcel/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Example parcel app",
3 | "name": "Example parcel starter template app",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/apps/example-parcel/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Layout } from './components/layout';
2 | import { Home } from './pages/home/home';
3 |
4 | export const App = () => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 | import { Box, mediaQueries } from '../design-system';
4 | import { ThemeSwitcher } from './theme-switcher';
5 |
6 | export const styles = {
7 | header: css`
8 | width: 100%;
9 | position: fixed;
10 | left: 0;
11 | top: 0;
12 | height: 4rem;
13 | display: flex;
14 | align-items: center;
15 | justify-content: flex-end;
16 | padding: 0 1rem;
17 | backdrop-filter: blur(1px);
18 | ${mediaQueries.medium} {
19 | backdrop-filter: none;
20 | }
21 | `,
22 | logo: css`
23 | pointer-events: none;
24 | justify-self: center;
25 | `,
26 | };
27 |
28 | export const Header = () => {
29 | return (
30 |
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import { Global } from '@emotion/react';
2 | import React from 'react';
3 | import { CSSReset, tokens } from '../design-system';
4 | import { Providers } from '../providers';
5 |
6 | export const Layout: React.FC = ({ children }) => {
7 | return (
8 |
9 |
10 |
18 | {children}
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/components/footer.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 | import { Box, Text, tokens } from '../design-system';
4 |
5 | export const styles = {
6 | footer: css`
7 | box-shadow: 0 1px 0 0 ${tokens.colors.border} inset;
8 | height: 5rem;
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | padding: 0 1rem;
13 | font-size: ${tokens.fontSizes.body};
14 | background: ${tokens.colors.codeBg};
15 | color: ${tokens.colors.codeText};
16 | `,
17 | };
18 |
19 | export const Footer = () => {
20 | return (
21 |
22 |
23 | Made by{' '}
24 |
46 | Dinesh
47 |
48 |
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/components/page.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 |
4 | const styles = {
5 | page: css`
6 | max-width: 960px;
7 | margin-left: auto;
8 | margin-right: auto;
9 | padding-left: 16px;
10 | padding-right: 16px;
11 | min-height: calc(100vh - 4rem);
12 | `,
13 | };
14 |
15 | export const Page: React.FC = ({ children }) => {
16 | return {children}
;
17 | };
18 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/components/theme-switcher.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import type { FC } from 'react';
3 | import { Button, useToggleTheme } from '../design-system';
4 |
5 | export const ThemeSwitcher: FC = () => {
6 | const { theme, toggleTheme } = useToggleTheme();
7 |
8 | return (
9 |
10 | {theme}
11 |
12 | );
13 | };
14 |
15 | if (process.env.NODE_ENV !== 'production') {
16 | ThemeSwitcher.displayName = 'ThemeSwitcher';
17 | }
18 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/design-system.ts:
--------------------------------------------------------------------------------
1 | /*
2 | Importing the design system imports from a local file because I keep changing
3 | how the local package is linked and it's easier to change all the import statements in one file
4 | */
5 | import {
6 | CSSReset,
7 | ThemeProvider,
8 | Box,
9 | Button,
10 | Text,
11 | Callout,
12 | themes,
13 | tokens,
14 | useToggleTheme,
15 | mediaQueries,
16 | } from '@unpublished/design-system';
17 |
18 | import type { Tokens } from '@unpublished/design-system';
19 |
20 | export {
21 | CSSReset,
22 | ThemeProvider,
23 | useToggleTheme,
24 | Box,
25 | Button,
26 | Text,
27 | Callout,
28 | themes,
29 | tokens,
30 | mediaQueries,
31 | };
32 |
33 | export type { Tokens };
34 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { App } from './app';
4 |
5 | ReactDOM.render(
6 |
7 |
8 | ,
9 | document.getElementById('app')
10 | );
11 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/pages/home/about.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 | import { Box, Callout } from '../../design-system';
4 |
5 | export const styles = {
6 | about: css`
7 | margin-top: 3rem;
8 | `,
9 | };
10 |
11 | export const About = () => {
12 | return (
13 |
14 |
15 | About section is written in MDX and MDX parsing is not setup for the
16 | parcel app yet.
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/pages/home/example-components.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 | import { Box, Text, Button, Callout, tokens } from '../../design-system';
4 |
5 | export const styles = {
6 | exampleComponents: css`
7 | margin: 2rem 0 0;
8 | `,
9 | };
10 |
11 | export const ExampleComponents = () => {
12 | return (
13 |
14 | Example Components
15 |
16 | Two example components built with tokens to demonstrate theming.
17 |
18 | Button
19 |
27 | Primary
28 | Secondary
29 |
30 | Callout
31 |
39 |
40 |
41 | Primary variant. You can write anything here to
42 | bring extra attention to your users.
43 |
44 |
45 |
46 |
47 | Secondary variant. You can write anything here to
48 | bring extra attention to your users.
49 |
50 |
51 |
52 |
53 | Ghost variant. You can write anything here to bring
54 | extra attention to your users.
55 |
56 |
57 |
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/pages/home/home.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 | import { Footer } from '../../components/footer';
4 | import { Header } from '../../components/header';
5 | import { Page } from '../../components/page';
6 | import { Box } from '../../design-system';
7 | import { About } from './about';
8 | import { ExampleComponents } from './example-components';
9 | import { Intro } from './intro';
10 |
11 | const styles = {
12 | homePage: css`
13 | min-height: 100vh;
14 | `,
15 | main: css`
16 | height: 100%;
17 | padding: 2rem 2rem;
18 | `,
19 | };
20 |
21 | export const Home = () => {
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/pages/home/intro.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 | import { Box, Text, tokens } from '../../design-system';
4 |
5 | export const styles = {
6 | intro: css`
7 | display: flex;
8 | flex-direction: column;
9 | align-items: center;
10 | justify-content: center;
11 | padding: 0 0.5rem;
12 | margin: 2rem 0 0;
13 | `,
14 | title: css`
15 | width: 100%;
16 | line-height: ${tokens.lineHeights.base};
17 | display: flex;
18 | flex-direction: column;
19 | justify-content: center;
20 | border: 1px solid;
21 | letter-spacing: -0.05em;
22 | padding: 1rem;
23 | border-color: ${tokens.colors.primaryBase};
24 | box-shadow: ${tokens.colors.primaryBase} 4px 4px 0 0;
25 | `,
26 | };
27 |
28 | export const Intro = () => {
29 | return (
30 |
31 |
32 | A Design System Template
33 |
43 | That Scales.
44 |
45 |
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/providers/index.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider, themes } from '../design-system';
2 |
3 | export const Providers: React.FC = ({ children }) => {
4 | return (
5 |
6 | {children}
7 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/apps/example-parcel/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/apps/example-parcel/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "allowJs": true,
6 | "allowSyntheticDefaultImports": true,
7 | "esModuleInterop": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "isolatedModules": true,
10 | "jsx": "react-jsx",
11 | "jsxImportSource": "@emotion/react",
12 | "lib": ["dom", "dom.iterable", "esnext"],
13 | "module": "esnext",
14 | "moduleResolution": "node",
15 | "noEmit": true,
16 | "noFallthroughCasesInSwitch": true,
17 | "resolveJsonModule": true,
18 | "skipLibCheck": true,
19 | "strict": true,
20 | "target": "esnext",
21 | },
22 | "include": ["src", "custom.d.ts"]
23 | }
24 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@babel/preset-typescript',
4 | [
5 | '@babel/preset-react',
6 | { runtime: 'automatic', importSource: '@emotion/react' },
7 | ],
8 | ],
9 | plugins: ['@emotion/babel-plugin'],
10 | };
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "design-system-boilerplate",
3 | "private": true,
4 | "repository": {
5 | "type": "git",
6 | "url": "https://github.com/flexdinesh/design-system-boilerplate "
7 | },
8 | "workspaces": [
9 | "apps/*",
10 | "packages/*"
11 | ],
12 | "scripts": {
13 | "clean:ds": "manypkg run @unpublished/design-system clean",
14 | "clean:next": "manypkg run @ds-boilerplate/nextjs-example clean",
15 | "clean:parcel": "manypkg run @ds-boilerplate/parcel-example clean",
16 | "clean:root": "rimraf --no-glob .parcel-cache tsconfig.tsbuildinfo ./node_modules/.cache",
17 | "clean": "yarn clean:root && yarn clean:ds && yarn clean:parcel && yarn clean:next",
18 | "build:next": "manypkg run @ds-boilerplate/nextjs-example build",
19 | "build:next:static": "manypkg run @ds-boilerplate/nextjs-example build:static",
20 | "dev:parcel": "manypkg run @ds-boilerplate/parcel-example dev",
21 | "dev:next": "manypkg run @ds-boilerplate/nextjs-example dev",
22 | "build": "preconstruct build",
23 | "// dev": "// why isnt this working? preconstruct dev",
24 | "dev": "preconstruct dev",
25 | "watch": "preconstruct watch",
26 | "fix": "manypkg fix && preconstruct fix",
27 | "check": "manypkg check && preconstruct validate",
28 | "ds:storybook": "manypkg run @unpublished/design-system storybook",
29 | "typecheck:ds": "manypkg run @unpublished/design-system typecheck",
30 | "lint": "eslint . --ext .ts,.tsx,.js,.jsx",
31 | "lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix",
32 | "// postinstall": "preconstruct dev"
33 | },
34 | "engines": {
35 | "node": "^14.13.1 || >=16.0.0",
36 | "yarn": ">=1.22.0",
37 | "npm": "please-use-yarn"
38 | },
39 | "dependencies": {
40 | "@emotion/eslint-plugin": "^11.7.0",
41 | "@manypkg/cli": "^0.19.1",
42 | "@mdx-js/react": "^2.0.0",
43 | "@preconstruct/cli": "^2.1.5",
44 | "@typescript-eslint/eslint-plugin": "5.8.1",
45 | "@typescript-eslint/parser": "5.8.1",
46 | "eslint": "8.5.0",
47 | "eslint-config-prettier": "8.3.0",
48 | "eslint-import-resolver-typescript": "2.5.0",
49 | "eslint-plugin-import": "2.25.3",
50 | "eslint-plugin-jest": "25.3.2",
51 | "eslint-plugin-jest-formatting": "3.1.0",
52 | "eslint-plugin-jsx-a11y": "6.5.1",
53 | "eslint-plugin-prettier": "4.0.0",
54 | "eslint-plugin-react": "7.28.0",
55 | "eslint-plugin-react-hooks": "4.3.0",
56 | "eslint-plugin-regexp": "1.5.1",
57 | "eslint-plugin-storybook": "0.5.5",
58 | "eslint-plugin-testing-library": "5.0.1",
59 | "prettier": "2.5.1",
60 | "rimraf": "3.0.2",
61 | "typescript": "4.5.4"
62 | },
63 | "manypkg": {
64 | "defaultBranch": "main"
65 | },
66 | "preconstruct": {
67 | "packages": [
68 | "packages/design-system"
69 | ]
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/packages/design-system/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # build
4 | /storybook-static
5 | .parcel-cache
6 | build
7 | dist
8 | .parcel-cache
9 |
10 | # dependencies
11 | node_modules
12 |
13 | # testing
14 | /coverage
15 |
16 | # misc
17 | .DS_Store
18 | *.pem
19 |
--------------------------------------------------------------------------------
/packages/design-system/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
3 |
4 | const nodeLibsBrowser = require('node-libs-browser')
5 | nodeLibsBrowser.assert = require.resolve('browser-assert')
6 | nodeLibsBrowser.util = require.resolve('util')
7 |
8 | module.exports = {
9 | framework: '@storybook/react',
10 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(ts|tsx|js|jsx)'],
11 | typescript: {
12 | check: false,
13 | checkOptions: {},
14 | reactDocgen: 'react-docgen-typescript',
15 | reactDocgenTypescriptOptions: {
16 | shouldExtractLiteralValuesFromEnum: true,
17 | propFilter: (prop) =>
18 | prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,
19 | },
20 | },
21 |
22 | features: {
23 | // @link https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#using-the-v7-store
24 | storyStoreV7: false,
25 | // @link https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#emotion11-quasi-compatibility
26 | emotionAlias: false,
27 | // @link https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#babel-mode-v7
28 | babelModeV7: false,
29 | },
30 | babel: (config) => {
31 | // config.presets.push(require.resolve('@emotion/babel-preset-css-prop'));
32 | return config;
33 | },
34 | webpackFinal: async (config) => {
35 | // Emotion 11 hacks
36 |
37 | // const emotionReactEleven = path.dirname(
38 | // require.resolve('@emotion/react/package.json')
39 | // );
40 | // const emotionStyledEleven = path.dirname(
41 | // require.resolve('@emotion/styled/package.json')
42 | // );
43 |
44 | return {
45 | ...config,
46 | resolve: {
47 | ...config.resolve,
48 | plugins: [...config.resolve.plugins, new TsconfigPathsPlugin({})],
49 | alias: {
50 | ...config.resolve.alias,
51 | // '@emotion/core': emotionReactEleven,
52 | // '@emotion/styled': emotionStyledEleven,
53 | // 'emotion-theming': emotionReactEleven,
54 | // inherits: 'inherits/inherits_browser.js',
55 | // superagent: 'superagent/lib/client',
56 | // emitter: 'component-emitter',
57 | },
58 | // mainFields: [...config.resolve.mainFields, 'browser', 'module', 'main'],
59 | },
60 | };
61 | },
62 |
63 | addons: [
64 | '@storybook/addon-links',
65 | '@storybook/addon-essentials',
66 | /*
67 | {
68 | name: '@storybook/addon-storysource',
69 | options: {
70 | loaderOptions: {
71 | injectStoryParameters: true,
72 | },
73 | },
74 | },*/
75 | // {
76 | // name: '@storybook/addon-postcss',
77 | // options: {
78 | // postcssLoaderOptions: {
79 | // implementation: require('postcss'),
80 | // },
81 | // },
82 | // },
83 | ],
84 | };
85 |
--------------------------------------------------------------------------------
/packages/design-system/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | export const parameters = {
2 | layout: 'fullscreen',
3 | actions: { argTypesRegex: '^on[A-Z].*' },
4 | controls: {
5 | matchers: {
6 | color: /(background|color)$/i,
7 | date: /Date$/,
8 | },
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/packages/design-system/README.md:
--------------------------------------------------------------------------------
1 | # Design System
2 |
3 | ## Intro
4 |
5 | A basic example of a design-system setup with storybook.
6 |
--------------------------------------------------------------------------------
/packages/design-system/jest.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | const { defaults: tsjPreset } = require('ts-jest/presets');
3 | const { pathsToModuleNameMapper } = require('ts-jest');
4 |
5 | const packageJson = require('./package.json');
6 | const { compilerOptions: baseTsConfig } = require('./tsconfig.json');
7 |
8 | // Take the paths from tsconfig automatically from base tsconfig.json
9 | // @link https://kulshekhar.github.io/ts-jest/docs/paths-mapping
10 | const getTsConfigBasePaths = () => {
11 | return baseTsConfig.paths
12 | ? pathsToModuleNameMapper(baseTsConfig.paths, {
13 | prefix: '/',
14 | })
15 | : {};
16 | };
17 |
18 | /** @typedef {import('ts-jest/dist/types')} */
19 | /** @type {import('@jest/types').Config.InitialOptions} */
20 | const config = {
21 | name: `${packageJson.name}:unit`,
22 | testEnvironment: 'jsdom',
23 | verbose: true,
24 | rootDir: './src',
25 | transform: {
26 | ...tsjPreset.transform,
27 | },
28 | setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
29 | testMatch: ['/**/*.{spec,test}.{js,jsx,ts,tsx}'],
30 | moduleNameMapper: {
31 | // For @testing-library/react
32 | // '^@/test-utils$': '/../config/jest/test-utils',
33 | ...getTsConfigBasePaths(),
34 | },
35 | // false by default, overrides in cli, ie: yarn test:unit --collect-coverage=true
36 | collectCoverage: false,
37 | coverageDirectory: '/../coverage',
38 | collectCoverageFrom: ['/**/*.{ts,tsx,js,jsx}', '!**/*.test.ts'],
39 | globals: {
40 | 'ts-jest': {
41 | diagnostics: true,
42 | tsconfig: './tsconfig.jest.json',
43 | },
44 | },
45 | };
46 |
47 | module.exports = config;
48 |
--------------------------------------------------------------------------------
/packages/design-system/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@unpublished/design-system",
3 | "version": "0.0.1",
4 | "license": "MIT",
5 | "private": true,
6 | "main": "dist/unpublished-design-system.cjs.js",
7 | "module": "dist/unpublished-design-system.esm.js",
8 | "types": "dist/unpublished-design-system.cjs.d.ts",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/flexdinesh/design-system-boilerplate",
12 | "directory": "packages/design-system"
13 | },
14 | "scripts": {
15 | "clean": "rimraf --no-glob ./dist ./build ./.parcel-cache ./tsconfig.tsbuildinfo ./node_modules/.cache ",
16 | "typecheck": "tsc --project ./tsconfig.json --noEmit",
17 | "test": "run-s 'test:*'",
18 | "test:unit": "jest --config jest.config.js --maxWorkers=50% --color $@",
19 | "storybook": "start-storybook -p 6006",
20 | "build:storybook": "build-storybook --output-dir build/storybook"
21 | },
22 | "devDependencies": {
23 | "@babel/core": "7.16.5",
24 | "@babel/preset-typescript": "^7.16.7",
25 | "@emotion/babel-plugin": "11.7.2",
26 | "@emotion/babel-preset-css-prop": "11.2.0",
27 | "@emotion/react": "^11.7.1",
28 | "@emotion/styled": "^11.6.0",
29 | "@storybook/addon-actions": "^6.4.16",
30 | "@storybook/addon-docs": "^6.4.16",
31 | "@storybook/addon-essentials": "^6.4.16",
32 | "@storybook/addon-links": "^6.4.16",
33 | "@storybook/addon-postcss": "^2.0.0",
34 | "@storybook/addon-storysource": "^6.4.16",
35 | "@storybook/react": "^6.4.16",
36 | "@testing-library/jest-dom": "^5.16.1",
37 | "@testing-library/react": "^12.1.2",
38 | "@testing-library/react-hooks": "7.0.2",
39 | "@types/jest": "^27.0.3",
40 | "@types/node": "^17.0.10",
41 | "@types/react": "17.0.38",
42 | "@types/react-dom": "^17.0.11",
43 | "@types/styled-system": "^5.1.14",
44 | "autoprefixer": "10.4.0",
45 | "babel-loader": "8.2.3",
46 | "browser-assert": "^1.2.1",
47 | "cross-env": "7.0.3",
48 | "jest": "27.4.5",
49 | "npm-run-all": "4.1.5",
50 | "react": "^17.0.2",
51 | "react-dom": "^17.0.2",
52 | "rimraf": "3.0.2",
53 | "ts-jest": "27.1.2",
54 | "tsconfig-paths-webpack-plugin": "^3.5.2",
55 | "typescript": "4.5.4",
56 | "typescript-styled-plugin": "^0.18.2",
57 | "webpack": "5.65.0"
58 | },
59 | "dependencies": {
60 | "just-clone": "^5.0.1",
61 | "just-safe-set": "^4.0.2",
62 | "styled-system": "^5.1.5"
63 | },
64 | "peerDependencies": {
65 | "@emotion/react": "^11.7.1",
66 | "@emotion/styled": "^11.6.0",
67 | "react": "^17.0.2",
68 | "react-dom": "^17.0.2"
69 | },
70 | "peerDependenciesMeta": {
71 | "@emotion/react": {
72 | "optional": false
73 | },
74 | "@emotion/styled": {
75 | "optional": false
76 | },
77 | "react": {
78 | "optional": false
79 | },
80 | "react-dom": {
81 | "optional": false
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/Box/Box.stories.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import type { ComponentStory, ComponentMeta } from '@storybook/react';
3 | import { args } from '../../storybook-helpers/args';
4 | import { StorybookLayout } from '../../storybook-helpers/StorybookLayout';
5 | import { Box } from './Box';
6 |
7 | export default {
8 | title: 'Components/Box',
9 | component: Box,
10 | argTypes: {
11 | backgroundColor: args.backgroundColor,
12 | },
13 | } as ComponentMeta;
14 |
15 | const Template: ComponentStory = (args) => {
16 | return (
17 |
18 | Box w:400, height:100
19 |
20 | );
21 | };
22 |
23 | export const BoxProps = Template.bind({});
24 | BoxProps.args = {
25 | border: '1px solid',
26 | borderColor: 'primary500',
27 | width: 400,
28 | height: 100,
29 | padding: 'medium',
30 | display: 'flex',
31 | alignItems: 'center',
32 | justifyContent: 'center',
33 | };
34 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/Box/Box.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import styled from '@emotion/styled';
3 | import type { ComponentProps } from 'react';
4 | import {
5 | border,
6 | color,
7 | display,
8 | flexbox,
9 | grid,
10 | layout,
11 | margin,
12 | overflow,
13 | padding,
14 | position,
15 | shadow,
16 | space,
17 | zIndex,
18 | } from 'styled-system';
19 | import { tokens } from '../../core/theme';
20 | import type {
21 | ThemeBordersProps,
22 | ThemeColorProps,
23 | ThemeDisplayProps,
24 | ThemeFlexboxProps,
25 | ThemeGridProps,
26 | ThemeLayoutProps,
27 | ThemeOverflowProps,
28 | ThemePositionProps,
29 | ThemeShadowProps,
30 | ThemeSpaceProps,
31 | ThemeZIndexProps,
32 | } from '../../core/types';
33 |
34 | interface Props
35 | extends ThemeBordersProps,
36 | ThemeColorProps,
37 | ThemeDisplayProps,
38 | ThemeFlexboxProps,
39 | ThemeGridProps,
40 | ThemeLayoutProps,
41 | ThemeOverflowProps,
42 | ThemePositionProps,
43 | ThemeShadowProps,
44 | ThemeSpaceProps,
45 | ThemeZIndexProps {
46 | children: React.ReactNode;
47 | }
48 |
49 | export const Box = styled('div')(
50 | border,
51 | color,
52 | display,
53 | flexbox,
54 | grid,
55 | layout,
56 | margin,
57 | overflow,
58 | padding,
59 | position,
60 | shadow,
61 | space,
62 | zIndex,
63 | {
64 | fontFamily: tokens.fontFamily.body,
65 | color: tokens.colors.textBody,
66 | }
67 | );
68 |
69 | if (process.env.NODE_ENV !== 'production') {
70 | Box.displayName = 'Box';
71 | }
72 |
73 | export type BoxProps = ComponentProps;
74 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/Box/index.ts:
--------------------------------------------------------------------------------
1 | export { Box } from './Box';
2 | export type { BoxProps } from './Box';
3 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/Button/Button.stories.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import type { ComponentStory, ComponentMeta } from '@storybook/react';
3 | import { StorybookLayout } from '../../storybook-helpers/StorybookLayout';
4 | import { Box } from '../Box/Box';
5 | import { Button } from './Button';
6 |
7 | export default {
8 | title: 'Components/Button',
9 | component: Button,
10 | } as ComponentMeta;
11 |
12 | const Template: ComponentStory = (args) => {
13 | return (
14 |
15 |
23 |
24 | Primary
25 |
26 |
27 | Secondary
28 |
29 |
30 | Secondary
31 |
32 |
33 |
41 |
42 | Primary
43 |
44 |
45 | Secondary
46 |
47 |
48 | Secondary
49 |
50 |
51 |
52 | );
53 | };
54 |
55 | export const Buttons = Template.bind({});
56 | Buttons.args = {};
57 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/Button/Button.styles.ts:
--------------------------------------------------------------------------------
1 | import { css, CSSObject } from '@emotion/react';
2 | import { tokens } from '../../core/theme';
3 | import type { ButtonProps } from './Button';
4 |
5 | export const variantStyles: {
6 | [variant in NonNullable]: CSSObject;
7 | } = {
8 | primary: {
9 | backgroundColor: tokens.colors.primaryBase,
10 | color: tokens.colors.textAlternate,
11 | ':hover': {
12 | backgroundColor: tokens.colors.primary400,
13 | },
14 | },
15 | secondary: {
16 | backgroundColor: tokens.colors.secondaryBase,
17 | color: tokens.colors.textAlternate,
18 | ':hover': {
19 | backgroundColor: tokens.colors.secondary400,
20 | },
21 | },
22 | };
23 |
24 | export const sizeStyles: {
25 | [variant in NonNullable]: CSSObject;
26 | } = {
27 | small: {
28 | minWidth: 80,
29 | },
30 | medium: {
31 | minWidth: 120,
32 | lineHeight: tokens.lineHeights.base,
33 | },
34 | large: {
35 | minWidth: 148,
36 | lineHeight: tokens.lineHeights.body,
37 | },
38 | };
39 |
40 | const baseStyles: CSSObject = {
41 | border: 0,
42 | borderRadius: tokens.radii.medium,
43 | fontSize: tokens.fontSizes.small,
44 | fontWeight: tokens.fontWeights.semibold,
45 | padding: '0.75rem 1rem',
46 | textAlign: 'center',
47 | cursor: 'pointer',
48 | userSelect: 'none',
49 | touchAction: 'manipulation',
50 |
51 | ':active': {
52 | transform: 'scale(0.96)',
53 | },
54 | };
55 |
56 | export const getStyles = ({
57 | variant,
58 | size,
59 | }: Pick): ReturnType => {
60 | const styles = {
61 | ...baseStyles,
62 | ...(variant && variantStyles[variant]),
63 | ...(size && sizeStyles[size]),
64 | };
65 |
66 | return css(styles);
67 | };
68 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { useMemo } from 'react';
3 | import { Box } from '../Box/Box';
4 | import type { BoxProps } from '../Box/Box';
5 | import { getStyles } from './Button.styles';
6 |
7 | export type ButtonProps = Omit & {
8 | variant?: 'primary' | 'secondary';
9 | size?: 'small' | 'medium' | 'large';
10 | };
11 |
12 | export const Button: React.FC = (props) => {
13 | const { children, variant = 'primary', size = 'medium', ...rest } = props;
14 |
15 | const styles = useMemo(() => {
16 | return getStyles({
17 | variant,
18 | size,
19 | });
20 | }, [variant, size]);
21 |
22 | return (
23 |
24 | {children}
25 |
26 | );
27 | };
28 |
29 | if (process.env.NODE_ENV !== 'production') {
30 | Button.displayName = 'Button';
31 | }
32 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/Button/index.ts:
--------------------------------------------------------------------------------
1 | export { Button } from './Button';
2 | export type { ButtonProps } from './Button';
3 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/CSSReset/CSSReset.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css, Global } from '@emotion/react';
3 | import { reset } from './reset';
4 |
5 | const anotherReset = css`
6 | html,
7 | body,
8 | div,
9 | span,
10 | applet,
11 | object,
12 | iframe,
13 | h1,
14 | h2,
15 | h3,
16 | h4,
17 | h5,
18 | h6,
19 | p,
20 | blockquote,
21 | pre,
22 | a,
23 | abbr,
24 | acronym,
25 | address,
26 | big,
27 | cite,
28 | code,
29 | del,
30 | dfn,
31 | em,
32 | img,
33 | ins,
34 | kbd,
35 | q,
36 | s,
37 | samp,
38 | small,
39 | strike,
40 | strong,
41 | sub,
42 | sup,
43 | tt,
44 | var,
45 | b,
46 | u,
47 | i,
48 | center,
49 | dl,
50 | dt,
51 | dd,
52 | fieldset,
53 | form,
54 | label,
55 | legend,
56 | table,
57 | caption,
58 | tbody,
59 | tfoot,
60 | thead,
61 | tr,
62 | th,
63 | td,
64 | article,
65 | aside,
66 | canvas,
67 | details,
68 | embed,
69 | figure,
70 | figcaption,
71 | footer,
72 | header,
73 | hgroup,
74 | menu,
75 | nav,
76 | output,
77 | ruby,
78 | section,
79 | summary,
80 | time,
81 | mark,
82 | audio,
83 | video {
84 | margin: 0;
85 | padding: 0;
86 | border: 0;
87 | font-size: 100%;
88 | font: inherit;
89 | vertical-align: baseline;
90 | }
91 |
92 | /* HTML5 display-role reset for older browsers */
93 | article,
94 | aside,
95 | details,
96 | figcaption,
97 | figure,
98 | footer,
99 | header,
100 | hgroup,
101 | menu,
102 | nav,
103 | section {
104 | display: block;
105 | }
106 | blockquote,
107 | q {
108 | quotes: none;
109 | }
110 | blockquote:before,
111 | blockquote:after,
112 | q:before,
113 | q:after {
114 | content: '';
115 | content: none;
116 | }
117 | table {
118 | border-collapse: collapse;
119 | border-spacing: 0;
120 | }
121 | `;
122 |
123 | export const CSSReset = () => {
124 | return (
125 |
131 | );
132 | };
133 |
134 | if (process.env.NODE_ENV !== 'production') {
135 | CSSReset.displayName = 'CSSReset';
136 | }
137 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/CSSReset/index.ts:
--------------------------------------------------------------------------------
1 | export { CSSReset } from './CSSReset';
2 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/CSSReset/reset.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/react';
2 |
3 | export const reset = css`
4 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
5 |
6 | /* Document
7 | ========================================================================== */
8 |
9 | /**
10 | * 1. Correct the line height in all browsers.
11 | * 2. Prevent adjustments of font size after orientation changes in iOS.
12 | */
13 |
14 | html {
15 | line-height: 1.15; /* 1 */
16 | -webkit-text-size-adjust: 100%; /* 2 */
17 | }
18 |
19 | /* Sections
20 | ========================================================================== */
21 |
22 | /**
23 | * Remove the margin in all browsers.
24 | */
25 |
26 | body {
27 | margin: 0;
28 | }
29 |
30 | /**
31 | * Render the 'main' element consistently in IE.
32 | */
33 |
34 | main {
35 | display: block;
36 | }
37 |
38 | /**
39 | * Correct the font size and margin on 'h1' elements within 'section' and
40 | * 'article' contexts in Chrome, Firefox, and Safari.
41 | */
42 |
43 | h1 {
44 | font-size: 2em;
45 | margin: 0.67em 0;
46 | }
47 |
48 | /* Grouping content
49 | ========================================================================== */
50 |
51 | /**
52 | * 1. Add the correct box sizing in Firefox.
53 | * 2. Show the overflow in Edge and IE.
54 | */
55 |
56 | hr {
57 | box-sizing: content-box; /* 1 */
58 | height: 0; /* 1 */
59 | overflow: visible; /* 2 */
60 | }
61 |
62 | /**
63 | * 1. Correct the inheritance and scaling of font size in all browsers.
64 | * 2. Correct the odd 'em' font sizing in all browsers.
65 | */
66 |
67 | pre {
68 | font-family: monospace, monospace; /* 1 */
69 | font-size: 1em; /* 2 */
70 | }
71 |
72 | /* Text-level semantics
73 | ========================================================================== */
74 |
75 | /**
76 | * Remove the gray background on active links in IE 10.
77 | */
78 |
79 | a {
80 | background-color: transparent;
81 | }
82 |
83 | /**
84 | * 1. Remove the bottom border in Chrome 57-
85 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
86 | */
87 |
88 | abbr[title] {
89 | border-bottom: none; /* 1 */
90 | text-decoration: underline; /* 2 */
91 | text-decoration: underline dotted; /* 2 */
92 | }
93 |
94 | /**
95 | * Add the correct font weight in Chrome, Edge, and Safari.
96 | */
97 |
98 | b,
99 | strong {
100 | font-weight: bolder;
101 | }
102 |
103 | /**
104 | * 1. Correct the inheritance and scaling of font size in all browsers.
105 | * 2. Correct the odd 'em' font sizing in all browsers.
106 | */
107 |
108 | code,
109 | kbd,
110 | samp {
111 | font-family: monospace, monospace; /* 1 */
112 | font-size: 1em; /* 2 */
113 | }
114 |
115 | /**
116 | * Add the correct font size in all browsers.
117 | */
118 |
119 | small {
120 | font-size: 80%;
121 | }
122 |
123 | /**
124 | * Prevent 'sub' and 'sup' elements from affecting the line height in
125 | * all browsers.
126 | */
127 |
128 | sub,
129 | sup {
130 | font-size: 75%;
131 | line-height: 0;
132 | position: relative;
133 | vertical-align: baseline;
134 | }
135 |
136 | sub {
137 | bottom: -0.25em;
138 | }
139 |
140 | sup {
141 | top: -0.5em;
142 | }
143 |
144 | /* Embedded content
145 | ========================================================================== */
146 |
147 | /**
148 | * Remove the border on images inside links in IE 10.
149 | */
150 |
151 | img {
152 | border-style: none;
153 | }
154 |
155 | /* Forms
156 | ========================================================================== */
157 |
158 | /**
159 | * 1. Change the font styles in all browsers.
160 | * 2. Remove the margin in Firefox and Safari.
161 | */
162 |
163 | button,
164 | input,
165 | optgroup,
166 | select,
167 | textarea {
168 | font-family: inherit; /* 1 */
169 | font-size: 100%; /* 1 */
170 | line-height: 1.15; /* 1 */
171 | margin: 0; /* 2 */
172 | }
173 |
174 | /**
175 | * Show the overflow in IE.
176 | * 1. Show the overflow in Edge.
177 | */
178 |
179 | button,
180 | input {
181 | /* 1 */
182 | overflow: visible;
183 | }
184 |
185 | /**
186 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
187 | * 1. Remove the inheritance of text transform in Firefox.
188 | */
189 |
190 | button,
191 | select {
192 | /* 1 */
193 | text-transform: none;
194 | }
195 |
196 | /**
197 | * Correct the inability to style clickable types in iOS and Safari.
198 | */
199 |
200 | button,
201 | [type='button'],
202 | [type='reset'],
203 | [type='submit'] {
204 | -webkit-appearance: button;
205 | }
206 |
207 | /**
208 | * Remove the inner border and padding in Firefox.
209 | */
210 |
211 | button::-moz-focus-inner,
212 | [type='button']::-moz-focus-inner,
213 | [type='reset']::-moz-focus-inner,
214 | [type='submit']::-moz-focus-inner {
215 | border-style: none;
216 | padding: 0;
217 | }
218 |
219 | /**
220 | * Restore the focus styles unset by the previous rule.
221 | */
222 |
223 | button:-moz-focusring,
224 | [type='button']:-moz-focusring,
225 | [type='reset']:-moz-focusring,
226 | [type='submit']:-moz-focusring {
227 | outline: 1px dotted ButtonText;
228 | }
229 |
230 | /**
231 | * Correct the padding in Firefox.
232 | */
233 |
234 | fieldset {
235 | padding: 0.35em 0.75em 0.625em;
236 | }
237 |
238 | /**
239 | * 1. Correct the text wrapping in Edge and IE.
240 | * 2. Correct the color inheritance from 'fieldset' elements in IE.
241 | * 3. Remove the padding so developers are not caught out when they zero out
242 | * 'fieldset' elements in all browsers.
243 | */
244 |
245 | legend {
246 | box-sizing: border-box; /* 1 */
247 | color: inherit; /* 2 */
248 | display: table; /* 1 */
249 | max-width: 100%; /* 1 */
250 | padding: 0; /* 3 */
251 | white-space: normal; /* 1 */
252 | }
253 |
254 | /**
255 | * Add the correct vertical alignment in Chrome, Firefox, and Opera.
256 | */
257 |
258 | progress {
259 | vertical-align: baseline;
260 | }
261 |
262 | /**
263 | * Remove the default vertical scrollbar in IE 10+.
264 | */
265 |
266 | textarea {
267 | overflow: auto;
268 | }
269 |
270 | /**
271 | * 1. Add the correct box sizing in IE 10.
272 | * 2. Remove the padding in IE 10.
273 | */
274 |
275 | [type='checkbox'],
276 | [type='radio'] {
277 | box-sizing: border-box; /* 1 */
278 | padding: 0; /* 2 */
279 | }
280 |
281 | /**
282 | * Correct the cursor style of increment and decrement buttons in Chrome.
283 | */
284 |
285 | [type='number']::-webkit-inner-spin-button,
286 | [type='number']::-webkit-outer-spin-button {
287 | height: auto;
288 | }
289 |
290 | /**
291 | * 1. Correct the odd appearance in Chrome and Safari.
292 | * 2. Correct the outline style in Safari.
293 | */
294 |
295 | [type='search'] {
296 | -webkit-appearance: textfield; /* 1 */
297 | outline-offset: -2px; /* 2 */
298 | }
299 |
300 | /**
301 | * Remove the inner padding in Chrome and Safari on macOS.
302 | */
303 |
304 | [type='search']::-webkit-search-decoration {
305 | -webkit-appearance: none;
306 | }
307 |
308 | /**
309 | * 1. Correct the inability to style clickable types in iOS and Safari.
310 | * 2. Change font properties to 'inherit' in Safari.
311 | */
312 |
313 | ::-webkit-file-upload-button {
314 | -webkit-appearance: button; /* 1 */
315 | font: inherit; /* 2 */
316 | }
317 |
318 | /* Interactive
319 | ========================================================================== */
320 |
321 | /*
322 | * Add the correct display in Edge, IE 10+, and Firefox.
323 | */
324 |
325 | details {
326 | display: block;
327 | }
328 |
329 | /*
330 | * Add the correct display in all browsers.
331 | */
332 |
333 | summary {
334 | display: list-item;
335 | }
336 |
337 | /* Misc
338 | ========================================================================== */
339 |
340 | /**
341 | * Add the correct display in IE 10+.
342 | */
343 |
344 | template {
345 | display: none;
346 | }
347 |
348 | /**
349 | * Add the correct display in IE 10.
350 | */
351 |
352 | [hidden] {
353 | display: none;
354 | }
355 | `;
356 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/Callout/Callout.stories.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import type { ComponentStory, ComponentMeta } from '@storybook/react';
3 | import { StorybookLayout } from '../../storybook-helpers/StorybookLayout';
4 | import { Box } from '../Box/Box';
5 | import { Text } from '../Text/Text';
6 | import { Callout } from './Callout';
7 |
8 | export default {
9 | title: 'Components/Callout',
10 | component: Callout,
11 | } as ComponentMeta;
12 |
13 | const Template: ComponentStory = (args) => {
14 | return (
15 |
16 |
21 |
22 |
23 | This is a callout. You can write anything here to
24 | bring extra attention to your users.
25 |
26 |
27 |
28 |
29 | This is a callout. You can write anything here to
30 | bring extra attention to your users.
31 |
32 |
33 |
34 |
35 |
36 | This is a callout. You can write anything here to
37 | bring extra attention to your users.
38 |
39 |
40 |
41 |
42 | This is a callout. You can write anything here to
43 | bring extra attention to your users.
44 |
45 |
46 |
47 |
48 |
49 | This is a callout. You can write anything here to
50 | bring extra attention to your users.
51 |
52 |
53 |
54 |
55 | This is a callout. You can write anything here to
56 | bring extra attention to your users.
57 |
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | export const Callouts = Template.bind({});
65 | Callouts.args = {};
66 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/Callout/Callout.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css, CSSObject } from '@emotion/react';
3 | import React from 'react';
4 | import { tokens } from '../../core/theme';
5 | import { Box } from '../Box/Box';
6 |
7 | export type CalloutProps = {
8 | variant?: 'primary' | 'secondary' | 'ghost';
9 | spacing?: 'default' | 'fit';
10 | };
11 |
12 | export const styles: CSSObject = {
13 | fontSize: tokens.fontSizes.base,
14 | borderLeft: '4px solid',
15 | borderRadius: tokens.radii.medium,
16 | margin: '1rem 0',
17 | color: tokens.colors.textBody,
18 | };
19 |
20 | export const variantStyles: {
21 | [variant in NonNullable]: CSSObject;
22 | } = {
23 | primary: {
24 | borderLeftColor: tokens.colors.primaryBase,
25 | background: tokens.colors.transparentPrimaryBg,
26 | },
27 | secondary: {
28 | borderLeftColor: tokens.colors.secondaryBase,
29 | background: tokens.colors.transparentSecondaryBg,
30 | },
31 | ghost: {
32 | borderLeftColor: tokens.colors.primaryBase,
33 | backgroundColor: 'transparent',
34 | },
35 | };
36 |
37 | export const spacingStyles: {
38 | [spacing in NonNullable]: CSSObject;
39 | } = {
40 | default: {
41 | padding: '1rem 1.25rem',
42 | },
43 | fit: {
44 | padding: '0.25rem 1.25rem',
45 | },
46 | };
47 |
48 | export const Callout: React.FC = (props) => {
49 | const { children, variant = 'primary', spacing = 'default', ...rest } = props;
50 |
51 | const componentStyles = React.useMemo(() => {
52 | return css({
53 | ...styles,
54 | ...(variant && variantStyles[variant]),
55 | ...(spacing && spacingStyles[spacing]),
56 | });
57 | }, [variant, spacing]);
58 |
59 | return (
60 |
61 | {children}
62 |
63 | );
64 | };
65 |
66 | if (process.env.NODE_ENV !== 'production') {
67 | Callout.displayName = 'Callout';
68 | }
69 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/Callout/index.ts:
--------------------------------------------------------------------------------
1 | export { Callout } from './Callout';
2 | export type { CalloutProps } from './Callout';
3 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/Text/Text.stories.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import type { ComponentStory, ComponentMeta } from '@storybook/react';
3 | import { args } from '../../storybook-helpers/args';
4 | import { StorybookLayout } from '../../storybook-helpers/StorybookLayout';
5 | import { Box } from '../Box/Box';
6 | import { Text } from './Text';
7 |
8 | export default {
9 | title: 'Components/Text',
10 | component: Text,
11 | argTypes: {
12 | backgroundColor: args.backgroundColor,
13 | },
14 | } as ComponentMeta;
15 |
16 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
17 | const Template: ComponentStory = (args) => {
18 | return (
19 |
20 |
21 | {'I am a heading. '}
22 | {'I am a heading. '}
23 | {'I am a heading. '}
24 | {'I am a heading. '}
25 | {'I am a paragraph.
'}
26 | {'I am a body text.
'}
27 |
28 |
29 | );
30 | };
31 |
32 | export const Headings = Template.bind({});
33 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/Text/Text.styles.ts:
--------------------------------------------------------------------------------
1 | import { css, CSSObject } from '@emotion/react';
2 | import { tokens } from '../../core/theme';
3 | import type { TextProps } from './Text';
4 |
5 | const TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'p'];
6 |
7 | export const getTagStyles: {
8 | [key in typeof TAGS[number]]: CSSObject;
9 | } = {
10 | h1: {
11 | color: tokens.colors.textHeading,
12 | // lineHeight: tokens.lineHeights.body,
13 | fontWeight: tokens.fontWeights.extrabold,
14 | fontSize: '4rem',
15 | margin: '1.5rem 0 0',
16 | },
17 | h2: {
18 | color: tokens.colors.textHeading,
19 | lineHeight: tokens.lineHeights.base,
20 | fontWeight: tokens.fontWeights.extrabold,
21 | fontSize: '2.5rem',
22 | margin: '1.5rem 0 0',
23 | },
24 | h3: {
25 | color: tokens.colors.textHeading,
26 | lineHeight: tokens.lineHeights.base,
27 | fontWeight: tokens.fontWeights.semibold,
28 | fontSize: '1.75rem',
29 | margin: '1rem 0 0',
30 | },
31 | h4: {
32 | color: tokens.colors.textHeading,
33 | lineHeight: tokens.lineHeights.base,
34 | fontWeight: tokens.fontWeights.semibold,
35 | fontSize: '1.25rem',
36 | margin: '1rem 0 0',
37 | },
38 | h5: {
39 | color: tokens.colors.textHeading,
40 | lineHeight: tokens.lineHeights.base,
41 | fontWeight: tokens.fontWeights.semibold,
42 | fontSize: tokens.fontSizes.body,
43 | margin: '1rem 0 0',
44 | },
45 | p: {
46 | margin: '1rem 0',
47 | },
48 | };
49 |
50 | const baseStyles: CSSObject = {
51 | lineHeight: tokens.lineHeights.body,
52 | color: tokens.colors.textBody,
53 | };
54 |
55 | export const getStyles = ({
56 | as,
57 | }: Pick): ReturnType => {
58 | const textStyles = TAGS.includes(as as string) && getTagStyles[as as string];
59 |
60 | const styles = {
61 | ...baseStyles,
62 | ...textStyles,
63 | };
64 |
65 | return css(styles);
66 | };
67 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/Text/Text.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { useMemo } from 'react';
3 | import { Box } from '../Box/Box';
4 | import type { BoxProps } from '../Box/Box';
5 | import { getStyles } from './Text.styles';
6 |
7 | export type TextProps = BoxProps;
8 |
9 | export const Text: React.FC = ({
10 | as = 'span',
11 | color = 'textBody',
12 | ...props
13 | }) => {
14 | const styles = useMemo(() => {
15 | return getStyles({
16 | as,
17 | });
18 | }, [as]);
19 |
20 | return (
21 |
22 | {props.children}
23 |
24 | );
25 | };
26 | if (process.env.NODE_ENV !== 'production') {
27 | Text.displayName = 'Text';
28 | }
29 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/Text/index.ts:
--------------------------------------------------------------------------------
1 | export { Text } from './Text';
2 | export type { TextProps } from './Text';
3 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/ThemeProvider/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { ThemeProvider as EmotionThemeProvider, Global } from '@emotion/react';
3 | import React, {
4 | createContext,
5 | useState,
6 | useCallback,
7 | useMemo,
8 | useEffect,
9 | useContext,
10 | } from 'react';
11 | import { themes } from '../../core/theme';
12 | import type { Themes } from '../../core/types';
13 | import { isBrowser } from '../../util/helpers';
14 | import { globalBaseStyles } from './global-styles';
15 |
16 | /* just the theme name is enough since all theme css vars are already inserted into stylesheet */
17 | const themeNames = themes.map((t) => t.name);
18 |
19 | type ThemeContext = {
20 | theme: string;
21 | setTheme: ({ themeName }: { themeName: string }) => void;
22 | toggleTheme: () => void;
23 | };
24 |
25 | const defaultContext = {
26 | theme: themeNames[0],
27 | };
28 |
29 | const ThemeContext = createContext(
30 | defaultContext as ThemeContext
31 | );
32 |
33 | type ContextWrapperProps = {
34 | defaultTheme: string;
35 | };
36 |
37 | const ContextWrapper: React.FC = ({
38 | children,
39 | defaultTheme,
40 | }) => {
41 | let currentTheme = defaultTheme;
42 | if (isBrowser) {
43 | const themeAttrInBodyElem = document.body.getAttribute(`data-theme`);
44 | if (themeAttrInBodyElem) {
45 | currentTheme = themeAttrInBodyElem;
46 | }
47 | }
48 | const [theme, setTheme] = useState(currentTheme);
49 |
50 | const toggleTheme = useCallback(() => {
51 | const index = themeNames.indexOf(theme);
52 | let nextTheme = theme;
53 | if (index >= 0 && index < themeNames.length - 1) {
54 | nextTheme = themeNames[index + 1];
55 | } else {
56 | nextTheme = themeNames[0];
57 | }
58 | if (isBrowser) {
59 | document.body.setAttribute(`data-theme`, nextTheme);
60 | setTheme(nextTheme);
61 | }
62 | }, [theme, setTheme]);
63 |
64 | const _setTheme = useCallback(
65 | ({ themeName }: { themeName: string }) => {
66 | if (isBrowser) {
67 | document.body.setAttribute(`data-theme`, themeName);
68 | setTheme(themeName);
69 | }
70 | },
71 | [setTheme]
72 | );
73 |
74 | useEffect(() => {
75 | if (isBrowser) {
76 | const themeAttrInBodyElem = document.body.getAttribute(`data-theme`);
77 | if (themeAttrInBodyElem) {
78 | setTheme(themeAttrInBodyElem);
79 | }
80 | }
81 | }, []);
82 |
83 | const contextValue = useMemo(() => {
84 | return { theme, toggleTheme, setTheme: _setTheme };
85 | }, [theme, toggleTheme, _setTheme]);
86 |
87 | return (
88 |
89 | {children}
90 |
91 | );
92 | };
93 |
94 | export const useTheme = () => {
95 | const themeContext = useContext(ThemeContext);
96 | if (!themeContext.setTheme || !themeContext.toggleTheme) {
97 | throw new Error('Please setup ThemeProvider');
98 | }
99 |
100 | return themeContext;
101 | };
102 |
103 | type ThemeWrapperProps = {
104 | themes: Themes;
105 | defaultTheme: string;
106 | };
107 |
108 | const ThemeWrapper: React.FC = ({
109 | themes,
110 | defaultTheme,
111 | children,
112 | }) => {
113 | if (!themes?.length) {
114 | throw new Error('You forgot to setup ThemeProvider');
115 | }
116 |
117 | if (!themes.find((t) => t.name === defaultTheme)) {
118 | throw new Error(`There's no theme with name: ${defaultTheme}`);
119 | }
120 |
121 | useEffect(() => {
122 | const dataThemeInBodyElem = document.body.getAttribute(`data-theme`);
123 | if (!dataThemeInBodyElem) {
124 | document.body.setAttribute(`data-theme`, defaultTheme);
125 | }
126 | }, [defaultTheme]);
127 |
128 | const globalCssVariablesForAllThemes = useMemo(() => {
129 | const globalStyles = themes.reduce<{
130 | [key: string]: Record;
131 | }>((styleObject, theme) => {
132 | styleObject[`body[data-theme='${theme.name}']`] = theme.__cssVars;
133 | return styleObject;
134 | }, {});
135 | return globalStyles;
136 | }, [themes]);
137 |
138 | // all themes have the same tokens and values are same css variables
139 | // so it doesnt' really matter which theme we pass to emotion
140 | const theme = themes[0];
141 |
142 | return (
143 |
144 |
145 | {/* This is where we'd import CDN fonts */}
146 | {/* */}
151 |
152 | {children}
153 |
154 | );
155 | };
156 |
157 | type ThemeProviderProps = {
158 | themes: Themes;
159 | defaultTheme: string;
160 | };
161 |
162 | export const ThemeProvider: React.FC = ({
163 | themes,
164 | defaultTheme,
165 | children,
166 | }) => {
167 | return (
168 |
169 |
170 | {children}
171 |
172 |
173 | );
174 | };
175 | if (process.env.NODE_ENV !== 'production') {
176 | ThemeProvider.displayName = 'ThemeProvider';
177 | }
178 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/ThemeProvider/global-styles.ts:
--------------------------------------------------------------------------------
1 | import { tokens } from '../../core/theme';
2 |
3 | export const globalBaseStyles = {
4 | '*, ::before, ::after': {
5 | boxSizing: 'border-box',
6 | },
7 |
8 | 'html, body': {
9 | background: tokens.colors.background,
10 | color: tokens.colors.textBody,
11 | height: '100%',
12 | fontFamily: tokens.fontFamily.body,
13 | padding: 0,
14 | margin: 0,
15 | textRendering: 'geometricPrecision',
16 | WebkitTextSizeAdjust: 'none',
17 | WebkitFontSmoothing: 'antialiased',
18 | MozOsxFontSmoothing: 'grayscale',
19 | fontSize: tokens.fontSizes.body,
20 | },
21 | strong: {
22 | fontWeight: tokens.fontWeights.semibold,
23 | },
24 | em: {
25 | fontStyle: 'italic',
26 | },
27 | a: {
28 | textDecoration: 'none',
29 | color: tokens.colors.link,
30 | },
31 | '::selection': {
32 | textShadow: 'none',
33 | color: tokens.colors.primary900,
34 | backgroundColor: tokens.colors.primary100,
35 | },
36 | pre: {
37 | lineHeight: 1.4,
38 | },
39 | img: {
40 | height: 'auto',
41 | maxWidth: '100%',
42 | },
43 | button: {
44 | color: 'inherit',
45 | },
46 | 'a, button': {
47 | /* Anchors and buttons don't want double-tap on mobile to zoom */
48 | touchAction: 'manipulation',
49 | },
50 | ':focus:not(:focus-visible)': {
51 | outline: 'none',
52 | },
53 | '*:focus-visible, input:focus-visible, button:focus-visible, [type="submit"]:focus-visible':
54 | {
55 | outline: `1px dashed ${tokens.colors.link}`,
56 | outlineOffset: '3px',
57 | },
58 | 'input:focus-visible': {
59 | outlineStyle: 'solid',
60 | outlineWidth: '3px',
61 | outlineOffset: 0,
62 | },
63 | li: {
64 | listStyle: 'square',
65 | },
66 | } as const;
67 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/ThemeProvider/index.ts:
--------------------------------------------------------------------------------
1 | export { ThemeProvider } from './ThemeProvider';
2 |
--------------------------------------------------------------------------------
/packages/design-system/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { Button } from './Button/Button';
2 | export { Box } from './Box/Box';
3 | export { CSSReset } from './CSSReset/CSSReset';
4 | export { Text } from './Text/Text';
5 | export { Callout } from './Callout/Callout';
6 | export { ThemeProvider } from './ThemeProvider/ThemeProvider';
7 |
--------------------------------------------------------------------------------
/packages/design-system/src/core/style-presets.ts:
--------------------------------------------------------------------------------
1 | import { tokens } from './theme';
2 |
3 | export const stylePreset = {
4 | textGradient: {
5 | backgroundImage: `linear-gradient(
6 | 135deg,
7 | ${tokens.colors.primary300},
8 | ${tokens.colors.primary200}
9 | )`,
10 | backgroundClip: 'text',
11 | color: 'transparent',
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/packages/design-system/src/core/theme.ts:
--------------------------------------------------------------------------------
1 | import { breakpoints, mediaQueries } from '../tokens/breakpoints';
2 | import * as colors from '../tokens/colors';
3 | import { radii } from '../tokens/radii';
4 | import { space } from '../tokens/space';
5 | import {
6 | fontSizes,
7 | fontWeights,
8 | fontFamily,
9 | lineHeights,
10 | } from '../tokens/typography';
11 | import { createTheme } from '../util/create-theme';
12 |
13 | const light = createTheme({
14 | name: 'Light',
15 | breakpoints,
16 | mediaQueries,
17 | colors: colors.light,
18 | space,
19 | radii,
20 | fontSizes,
21 | fontWeights,
22 | fontFamily,
23 | lineHeights,
24 | });
25 |
26 | const lightOrange = createTheme({
27 | name: 'Light Orange',
28 | breakpoints,
29 | mediaQueries,
30 | colors: colors.lightOrange,
31 | space,
32 | radii,
33 | fontSizes,
34 | fontWeights,
35 | fontFamily,
36 | lineHeights,
37 | });
38 |
39 | const lightYellow = createTheme({
40 | name: 'Light Yellow',
41 | breakpoints,
42 | mediaQueries,
43 | colors: colors.lightYellow,
44 | space,
45 | radii,
46 | fontSizes,
47 | fontWeights,
48 | fontFamily,
49 | lineHeights,
50 | });
51 |
52 | const lightBrown = createTheme({
53 | name: 'Light Brown',
54 | breakpoints,
55 | mediaQueries,
56 | colors: colors.lightBrown,
57 | space,
58 | radii,
59 | fontSizes,
60 | fontWeights,
61 | fontFamily,
62 | lineHeights,
63 | });
64 |
65 | const paper = createTheme({
66 | name: 'Paper',
67 | breakpoints,
68 | mediaQueries,
69 | colors: colors.paper,
70 | space,
71 | radii,
72 | fontSizes,
73 | fontWeights,
74 | fontFamily,
75 | lineHeights,
76 | });
77 |
78 | const dark = createTheme({
79 | name: 'Dark',
80 | breakpoints,
81 | mediaQueries,
82 | colors: colors.dark,
83 | space,
84 | radii,
85 | fontSizes,
86 | fontWeights,
87 | fontFamily,
88 | lineHeights,
89 | });
90 |
91 | const darkHoney = createTheme({
92 | name: 'Dark Honey',
93 | breakpoints,
94 | mediaQueries,
95 | colors: colors.darkHoney,
96 | space,
97 | radii,
98 | fontSizes,
99 | fontWeights,
100 | fontFamily,
101 | lineHeights,
102 | });
103 |
104 | const darkPink = createTheme({
105 | name: 'Dark Pink',
106 | breakpoints,
107 | mediaQueries,
108 | colors: colors.darkPink,
109 | space,
110 | radii,
111 | fontSizes,
112 | fontWeights,
113 | fontFamily,
114 | lineHeights,
115 | });
116 |
117 | const darkOrange = createTheme({
118 | name: 'Dark Orange',
119 | breakpoints,
120 | mediaQueries,
121 | colors: colors.darkOrange,
122 | space,
123 | radii,
124 | fontSizes,
125 | fontWeights,
126 | fontFamily,
127 | lineHeights,
128 | });
129 |
130 | /* since we only use css vars as token values - theme keys have the same values across themes */
131 | export const tokens = light;
132 |
133 | export const themes = [
134 | light,
135 | lightOrange,
136 | lightYellow,
137 | lightBrown,
138 | paper,
139 | dark,
140 | darkHoney,
141 | darkPink,
142 | darkOrange,
143 | ];
144 |
--------------------------------------------------------------------------------
/packages/design-system/src/core/types.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | BordersProps,
3 | ColorProps,
4 | DisplayProps,
5 | FlexboxProps,
6 | GridProps,
7 | LayoutProps,
8 | OverflowProps,
9 | PositionProps,
10 | ShadowProps,
11 | SpaceProps,
12 | TypographyProps,
13 | ZIndexProps,
14 | } from 'styled-system';
15 | import { themes, tokens } from './theme';
16 |
17 | /* all themes have the same token keys */
18 | export type Themes = typeof themes;
19 | export type Theme = Themes[number];
20 | export type Tokens = Omit;
21 | export type ColorTokens = Tokens['colors'][keyof Tokens['colors']];
22 | export type SpaceTokens = Tokens['space'][keyof Tokens['space']];
23 |
24 | export type ThemeBordersProps = BordersProps;
25 | export type ThemeColorProps = ColorProps;
26 | export type ThemeDisplayProps = DisplayProps;
27 | export type ThemeFlexboxProps = FlexboxProps;
28 | export type ThemeGridProps = GridProps;
29 | export type ThemeLayoutProps = LayoutProps;
30 | export type ThemeOverflowProps = OverflowProps;
31 | export type ThemePositionProps = PositionProps;
32 | export type ThemeShadowProps = ShadowProps;
33 | export type ThemeSpaceProps = SpaceProps;
34 | export type ThemeTypographyProps = TypographyProps;
35 | export type ThemeZIndexProps = ZIndexProps;
36 |
--------------------------------------------------------------------------------
/packages/design-system/src/examples/Homepage/Intro.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 | import { Box, Text, tokens } from '../../';
4 |
5 | export const styles = {
6 | container: css`
7 | // height: 20rem;
8 | display: flex;
9 | flex-direction: column;
10 | align-items: center;
11 | justify-content: center;
12 | padding: 0 0.5rem;
13 | // border: 1px solid yellow;
14 | `,
15 | wrapper: css`
16 | display: flex;
17 | flex-direction: column;
18 | justify-content: center;
19 | border: 1px solid cornflowerblue;
20 | `,
21 | };
22 |
23 | export const Intro = () => {
24 | return (
25 |
26 |
27 |
33 | A design system template
34 |
35 |
42 | that scales
43 |
44 |
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/packages/design-system/src/examples/Homepage/Layout.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { Global } from '@emotion/react';
3 | import React from 'react';
4 | import { CSSReset, ThemeProvider, themes, tokens } from '../../';
5 |
6 | const Providers: React.FC = ({ children }) => {
7 | return (
8 |
9 | {children}
10 |
11 | );
12 | };
13 |
14 | export const Layout: React.FC = ({ children }) => {
15 | return (
16 |
17 |
18 |
26 | {children}
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/packages/design-system/src/examples/Homepage/Page.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 |
4 | const pageWrapper = css`
5 | max-width: 960px;
6 | margin-left: auto;
7 | margin-right: auto;
8 | padding-left: 16px;
9 | padding-right: 16px;
10 | `;
11 |
12 | export const Page: React.FC = ({ children }) => {
13 | return {children}
;
14 | };
15 |
--------------------------------------------------------------------------------
/packages/design-system/src/examples/Homepage/Principles.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import { css } from '@emotion/react';
3 | import { Box, Text } from '../../';
4 |
5 | export const styles = {
6 | container: css`
7 | margin-top: 2rem;
8 | `,
9 | };
10 |
11 | export const Principles = () => {
12 | return (
13 |
14 |
15 | Principles
16 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/packages/design-system/src/examples/Homepage/ThemeSwitcher.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import type { FC } from 'react';
3 | import { Button, useToggleTheme } from '../../';
4 |
5 | export const ThemeSwitcher: FC = () => {
6 | const { theme, toggleTheme } = useToggleTheme();
7 |
8 | return (
9 |
10 | {theme}
11 |
12 | );
13 | };
14 |
15 | if (process.env.NODE_ENV !== 'production') {
16 | ThemeSwitcher.displayName = 'ThemeSwitcher';
17 | }
18 |
--------------------------------------------------------------------------------
/packages/design-system/src/hooks/useToggleTheme.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { themes } from '../core/theme';
3 | import { isBrowser } from '../util/helpers';
4 |
5 | /* just the theme name is enough since all theme css vars are already inserted into stylesheet */
6 | const themeNames = themes.map((t) => t.name);
7 |
8 | const getLatestThemeFromBodyElem = (defaultTheme: string) => {
9 | if (isBrowser) {
10 | const themeAttrInBodyElem = document.body.getAttribute(`data-theme`);
11 | if (themeAttrInBodyElem) {
12 | return themeAttrInBodyElem;
13 | }
14 | }
15 | return defaultTheme;
16 | };
17 |
18 | /* This hook won't re-render the entire React tree when the theme changes */
19 | export const useToggleTheme = (defaultTheme = themeNames[0]) => {
20 | const currentTheme = getLatestThemeFromBodyElem(defaultTheme);
21 | const [theme, setTheme] = React.useState(currentTheme);
22 |
23 | const toggleTheme = React.useCallback(() => {
24 | const currentTheme = getLatestThemeFromBodyElem(theme);
25 |
26 | const index = themeNames.indexOf(currentTheme);
27 | let nextTheme = theme;
28 | if (index >= 0 && index < themeNames.length - 1) {
29 | nextTheme = themeNames[index + 1];
30 | } else {
31 | nextTheme = themeNames[0];
32 | }
33 | if (isBrowser) {
34 | document.body.setAttribute(`data-theme`, nextTheme);
35 | setTheme(nextTheme);
36 | }
37 | }, [theme, setTheme]);
38 |
39 | return { theme, toggleTheme };
40 | };
41 |
--------------------------------------------------------------------------------
/packages/design-system/src/index.ts:
--------------------------------------------------------------------------------
1 | /* Components */
2 | export { Box } from './components/Box/Box';
3 | export { Button } from './components/Button/Button';
4 | export { CSSReset } from './components/CSSReset/CSSReset';
5 | export { Text } from './components/Text/Text';
6 | export { Callout } from './components/Callout/Callout';
7 | export { ThemeProvider } from './components/ThemeProvider/ThemeProvider';
8 |
9 | /* Types */
10 | export type { BoxProps } from './components/Box/Box';
11 | export type { ButtonProps } from './components/Button/Button';
12 | export type { TextProps } from './components/Text/Text';
13 | export type { CalloutProps } from './components/Callout/Callout';
14 | export * from './types';
15 |
16 | /* Core */
17 | export { tokens, themes } from './core/theme';
18 | export { stylePreset } from './core/style-presets';
19 |
20 | /* Hooks */
21 | export { useToggleTheme } from './hooks/useToggleTheme';
22 | export { useTheme } from './components/ThemeProvider/ThemeProvider';
23 |
24 | /* Util */
25 | export { mediaQueries } from './tokens/breakpoints';
26 |
--------------------------------------------------------------------------------
/packages/design-system/src/storybook-helpers/StorybookLayout.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import React from 'react';
3 | import type { FC } from 'react';
4 | import { Box } from '../components/Box';
5 | import { Button } from '../components/Button';
6 | import { CSSReset } from '../components/CSSReset';
7 | import { ThemeProvider } from '../components/ThemeProvider';
8 | import { themes } from '../core/theme';
9 | import { useToggleTheme } from '../hooks/useToggleTheme';
10 |
11 | export const ThemeSwitcher: FC = () => {
12 | const { theme, toggleTheme } = useToggleTheme();
13 |
14 | return (
15 |
21 |
22 | {theme}
23 |
24 |
25 | );
26 | };
27 |
28 | if (process.env.NODE_ENV !== 'production') {
29 | ThemeSwitcher.displayName = 'ThemeSwitcher';
30 | }
31 |
32 | export const StorybookLayout: React.FC = ({ children }) => {
33 | return (
34 |
35 |
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/packages/design-system/src/storybook-helpers/args.ts:
--------------------------------------------------------------------------------
1 | import { tokens } from '../core/theme';
2 |
3 | export const args = {
4 | backgroundColor: {
5 | options: Object.keys(tokens.colors),
6 | control: { type: 'select' },
7 | description: 'Color token',
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/packages/design-system/src/tokens/breakpoints.ts:
--------------------------------------------------------------------------------
1 | /*
2 | 0em = 0px
3 | 40em = 640px
4 | 52em = 832px
5 | 64em = 1024px
6 | 75em = 1200px
7 | */
8 | const breakpointScale = ['0em', '40em', '52em', '64em', '75em'] as const;
9 |
10 | // breakpoint tokens - just demo values - find your right breakpoint
11 | const breakpoints = {
12 | // not having 'small' breakpoint forces us to build
13 | // for mobile devices and scale for bigger devices
14 | default: breakpointScale[0], // mobile phones and higher 0-639px
15 | medium: breakpointScale[1], // tablets and higher 640px-832px
16 | large: breakpointScale[2], // laptops and higher >832px
17 | };
18 |
19 | // media queries
20 | const mediaQueries = {
21 | medium: `@media screen and (min-width: ${breakpointScale[1]})`,
22 | large: `@media screen and (min-width: ${breakpointScale[2]})`,
23 | };
24 |
25 | export { breakpoints, mediaQueries };
26 |
--------------------------------------------------------------------------------
/packages/design-system/src/tokens/colors.ts:
--------------------------------------------------------------------------------
1 | // https://smart-swatch.netlify.app - for swatch generation
2 | // http://hex2rgba.devoth.com/ - hex to rgba
3 | // color tokens - just demo values - find your right colors
4 | const light = {
5 | primaryBase: '#3da9fc',
6 | primary50: '#ddf5ff',
7 | primary100: '#afdfff',
8 | primary200: '#7fc7fe',
9 | primary300: '#4fb1fc',
10 | primary400: '#249bfb',
11 | primary500: '#0f81e1',
12 | primary600: '#0365b0',
13 | primary700: '#00487f',
14 | primary800: '#002b4e',
15 | primary900: '#000f1f',
16 |
17 | secondaryBase: '#ef4565',
18 | secondary50: '#ffe5eb',
19 | secondary100: '#fdb7c5',
20 | secondary200: '#f6899e',
21 | secondary300: '#f15b77',
22 | secondary400: '#ec2e50',
23 | secondary500: '#d21537',
24 | secondary600: '#a40e2a',
25 | secondary700: '#76081e',
26 | secondary800: '#480211',
27 | secondary900: '#1e0005',
28 |
29 | transparentPrimaryBg: 'rgba(61, 169, 252, 0.05)',
30 | transparentSecondaryBg: 'rgba(239, 69, 101, 0.05)',
31 | background: '#fffffe',
32 | backgroundAlternate: '#000f1f',
33 | textHeading: '#094067',
34 | textBody: '#47546b',
35 | textAlternate: '#fffffe',
36 | codeText: '#47546b',
37 | codeBg: '#f6f7f9',
38 | border: '#e2e8f0',
39 | link: '#166bff',
40 | linkActive: '#0b55d5',
41 | };
42 |
43 | // color tokens - just demo values - find your right colors
44 | // https://www.happyhues.co/palettes/9
45 | const lightOrange = {
46 | primaryBase: '#ff8e3c',
47 | primary50: '#ffeedc',
48 | primary100: '#ffd2af',
49 | primary200: '#ffb57f',
50 | primary300: '#ff984d',
51 | primary400: '#fe7a1c',
52 | primary500: '#e56103',
53 | primary600: '#b34b00',
54 | primary700: '#813600',
55 | primary800: '#4f1f00',
56 | primary900: '#200800',
57 |
58 | secondaryBase: '#d9376e',
59 | secondary50: '#ffe6f0',
60 | secondary100: '#f8bdd0',
61 | secondary200: '#ed93b1',
62 | secondary300: '#e46892',
63 | secondary400: '#da3e73',
64 | secondary500: '#c1255a',
65 | secondary600: '#971a46',
66 | secondary700: '#6d1132',
67 | secondary800: '#43071e',
68 | secondary900: '#1c000b',
69 |
70 | transparentPrimaryBg: 'rgba(255, 142, 60, 0.05)',
71 | transparentSecondaryBg: 'rgba(217, 55, 110, 0.05)',
72 | background: '#eff0f3',
73 | backgroundAlternate: '#000f1f',
74 | textHeading: '#0d0d0d',
75 | textBody: '#2a2a2a',
76 | textAlternate: '#0d0d0d',
77 |
78 | codeText: '#47546b',
79 | codeBg: '#f6f7f9',
80 | border: '#e2e8f0',
81 | link: '#166bff',
82 | linkActive: '#0b55d5',
83 | };
84 |
85 | // https://www.happyhues.co/palettes/14
86 | const lightYellow = {
87 | primaryBase: '#ffd803',
88 | primary50: '#fffbda',
89 | primary100: '#fff3ad',
90 | primary200: '#ffeb7d',
91 | primary300: '#ffe34b',
92 | primary400: '#ffdb1a',
93 | primary500: '#e6c200',
94 | primary600: '#b39700',
95 | primary700: '#806c00',
96 | primary800: '#4d4100',
97 | primary900: '#1c1600',
98 |
99 | secondaryBase: '#bae8e8',
100 | secondary50: '#e5fbfb',
101 | secondary100: '#c6ecec',
102 | secondary200: '#a3dede',
103 | secondary300: '#80d1d1',
104 | secondary400: '#60c5c5',
105 | secondary500: '#48abab',
106 | secondary600: '#378585',
107 | secondary700: '#265f5f',
108 | secondary800: '#133939',
109 | secondary900: '#001414',
110 |
111 | transparentPrimaryBg: 'rgba(255, 216, 3, 0.05)',
112 | transparentSecondaryBg: 'rgba(186, 232, 232, 0.05)',
113 | background: '#fffffe',
114 | backgroundAlternate: '#1c1600',
115 | textHeading: '#272343',
116 | textBody: '#2d334a',
117 | textAlternate: '#272343',
118 | codeText: '#47546b',
119 | codeBg: '#f6f7f9',
120 | border: '#e2e8f0',
121 | link: '#166bff',
122 | linkActive: '#0b55d5',
123 | };
124 |
125 | // https://www.happyhues.co/palettes/11
126 | const lightBrown = {
127 | primaryBase: '#8c7851',
128 | primary50: '#fcf2e4',
129 | primary100: '#e5dcca',
130 | primary200: '#d1c5ab',
131 | primary300: '#bfad8d',
132 | primary400: '#ab966e',
133 | primary500: '#917d54',
134 | primary600: '#716140',
135 | primary700: '#52452d',
136 | primary800: '#312a18',
137 | primary900: '#140e00',
138 |
139 | secondaryBase: '#f25042',
140 | secondary50: '#ffe6e3',
141 | secondary100: '#febcb5',
142 | secondary200: '#f89087',
143 | secondary300: '#f46458',
144 | secondary400: '#ef3929',
145 | secondary500: '#d62110',
146 | secondary600: '#a7180b',
147 | secondary700: '#781007',
148 | secondary800: '#4a0702',
149 | secondary900: '#1f0000',
150 |
151 | transparentPrimaryBg: 'rgba(140, 120, 81, 0.05)',
152 | transparentSecondaryBg: 'rgba(242, 80, 66, 0.05)',
153 | background: '#f9f4ef',
154 | backgroundAlternate: '#140e00',
155 | textHeading: '#020826',
156 | textBody: '#716040',
157 | textAlternate: '#fffffe',
158 | codeText: '#47546b',
159 | codeBg: '#f6f7f9',
160 | border: '#e2e8f0',
161 | link: '#166bff',
162 | linkActive: '#0b55d5',
163 | };
164 |
165 | // color tokens - just demo values - find your right colors
166 | // I created this theme on my own - because I love black and white themes
167 | const paper = {
168 | primaryBase: '#47546b',
169 | primary50: '#eaf3ff',
170 | primary100: '#cfd7e4',
171 | primary200: '#b1bccd',
172 | primary300: '#94a1b8',
173 | primary400: '#7686a2',
174 | primary500: '#5d6c89',
175 | primary600: '#47546b',
176 | primary700: '#323c4e',
177 | primary800: '#1d2431',
178 | primary900: '#040d17',
179 |
180 | secondaryBase: '#94a5b8',
181 | secondary50: '#eaf7ff',
182 | secondary100: '#cfdbe4',
183 | secondary200: '#b1c0cd',
184 | secondary300: '#94a5b8',
185 | secondary400: '#7689a2',
186 | secondary500: '#5d6c89',
187 | secondary600: '#47546b',
188 | secondary700: '#323c4e',
189 | secondary800: '#1d2431',
190 | secondary900: '#040d17',
191 |
192 | transparentPrimaryBg: 'rgba(109, 112, 120, 0.05)',
193 | transparentSecondaryBg: 'rgba(148, 165, 184, 0.05)',
194 | background: '#fffffe',
195 | backgroundAlternate: '#000f1f',
196 | textHeading: '#094067',
197 | textBody: '#47546b',
198 | textAlternate: '#fffffe',
199 | codeText: '#47546b',
200 | codeBg: '#f6f7f9',
201 | border: '#e2e8f0',
202 | link: '#166bff',
203 | linkActive: '#0b55d5',
204 | };
205 |
206 | // https://www.happyhues.co/palettes/4
207 | const dark = {
208 | primaryBase: '#7f5af0',
209 | primary50: '#eee7ff',
210 | primary100: '#c9b8fc',
211 | primary200: '#a48af5',
212 | primary300: '#805bf0',
213 | primary400: '#5c2eeb',
214 | primary500: '#4314d1',
215 | primary600: '#340fa4',
216 | primary700: '#250a76',
217 | primary800: '#150548',
218 | primary900: '#08011d',
219 |
220 | secondaryBase: '#2cb67d',
221 | secondary50: '#dffdf2',
222 | secondary100: '#bbf1dc',
223 | secondary200: '#95e7c5',
224 | secondary300: '#6ddcaf',
225 | secondary400: '#46d298',
226 | secondary500: '#2db97f',
227 | secondary600: '#209063',
228 | secondary700: '#136746',
229 | secondary800: '#043f28',
230 | secondary900: '#00170a',
231 |
232 | transparentPrimaryBg: 'rgba(127, 90, 240, 0.05)',
233 | transparentSecondaryBg: 'rgba(44, 182, 125, 0.05)',
234 | background: '#171923',
235 | backgroundAlternate: '#fffffe',
236 | textHeading: '#fffffe',
237 | textBody: '#c9d1d9',
238 | textAlternate: '#fffffe',
239 | codeText: '#cbd5e0',
240 | codeBg: '#1f2631',
241 | border: '#2d3748',
242 | link: '#2997ff',
243 | linkActive: '#89b3fa',
244 | };
245 |
246 | // https://www.happyhues.co/palettes/10
247 | const darkHoney = {
248 | primaryBase: '#f9bc60',
249 | primary50: '#fff4dc',
250 | primary100: '#fee0b1',
251 | primary200: '#fbcb83',
252 | primary300: '#f9b753',
253 | primary400: '#f7a224',
254 | primary500: '#dd880c',
255 | primary600: '#ac6a06',
256 | primary700: '#7b4c03',
257 | primary800: '#4b2d00',
258 | primary900: '#1d0e00',
259 |
260 | secondaryBase: '#e16162',
261 | secondary50: '#ffe7e7',
262 | secondary100: '#f7bebe',
263 | secondary200: '#ec9395',
264 | secondary300: '#e3696a',
265 | secondary400: '#d93f40',
266 | secondary500: '#c02627',
267 | secondary600: '#961c1d',
268 | secondary700: '#6c1314',
269 | secondary800: '#42090b',
270 | secondary900: '#1c0000',
271 |
272 | transparentPrimaryBg: 'rgba(249, 188, 96, 0.05)',
273 | transparentSecondaryBg: 'rgba(225, 97, 98, 0.05)',
274 | background: '#004643',
275 | backgroundAlternate: '#e8e4e6',
276 | textHeading: '#fffffe',
277 | textBody: '#abd1c6',
278 | textAlternate: '#001e1d',
279 | codeText: '#cbd5e0',
280 | codeBg: '#1f2631',
281 | border: '#2d3748',
282 | link: '#2997ff',
283 | linkActive: '#89b3fa',
284 | };
285 |
286 | // https://www.happyhues.co/palettes/12
287 | const darkPink = {
288 | primaryBase: '#eebbc3',
289 | primary50: '#ffe9ec',
290 | primary100: '#f0c2c9',
291 | primary200: '#e49aa5',
292 | primary300: '#d87282',
293 | primary400: '#cd4b5e',
294 | primary500: '#b53345',
295 | primary600: '#8d2736',
296 | primary700: '#641b27',
297 | primary800: '#3d0f17',
298 | primary900: '#180307',
299 |
300 | secondaryBase: '#bae8e8',
301 | secondary50: '#e5fbfb',
302 | secondary100: '#c6ecec',
303 | secondary200: '#a3dede',
304 | secondary300: '#80d1d1',
305 | secondary400: '#60c5c5',
306 | secondary500: '#48abab',
307 | secondary600: '#378585',
308 | secondary700: '#265f5f',
309 | secondary800: '#133939',
310 | secondary900: '#001414',
311 |
312 | transparentPrimaryBg: 'rgba(238, 187, 195, 0.05)',
313 | transparentSecondaryBg: 'rgba(186, 232, 232, 0.05)',
314 | background: '#232946',
315 | backgroundAlternate: '#b8c1ec',
316 | textHeading: '#fffffe',
317 | textBody: '#b8c1ec',
318 | textAlternate: '#232946',
319 | codeText: '#cbd5e0',
320 | codeBg: '#1f2631',
321 | border: '#2d3748',
322 | link: '#2997ff',
323 | linkActive: '#89b3fa',
324 | };
325 |
326 | // https://www.happyhues.co/palettes/13
327 | const darkOrange = {
328 | primaryBase: '#ff8906',
329 | primary50: '#fff2da',
330 | primary100: '#ffdaae',
331 | primary200: '#ffc37d',
332 | primary300: '#ffaa4c',
333 | primary400: '#ff921a',
334 | primary500: '#e67900',
335 | primary600: '#b35e00',
336 | primary700: '#814200',
337 | primary800: '#4f2700',
338 | primary900: '#200b00',
339 |
340 | secondaryBase: '#e53170',
341 | secondary50: '#ffe4f0',
342 | secondary100: '#fbb9d1',
343 | secondary200: '#f38db0',
344 | secondary300: '#ec6091',
345 | secondary400: '#e53372',
346 | secondary500: '#cc1a58',
347 | secondary600: '#9f1144',
348 | secondary700: '#730a31',
349 | secondary800: '#47041d',
350 | secondary900: '#1e000b',
351 |
352 | transparentPrimaryBg: 'rgba(255, 137, 6, 0.05)',
353 | transparentSecondaryBg: 'rgba(229, 49, 112, 0.05)',
354 | background: '#0f0e17',
355 | backgroundAlternate: '#fffffe',
356 | textHeading: '#fffffe',
357 | textBody: '#a7a9be',
358 | textAlternate: '#fffffe',
359 | codeText: '#cbd5e0',
360 | codeBg: '#1f2631',
361 | border: '#2d3748',
362 | link: '#2997ff',
363 | linkActive: '#89b3fa',
364 | };
365 |
366 | export {
367 | light,
368 | lightYellow,
369 | lightOrange,
370 | lightBrown,
371 | paper,
372 | dark,
373 | darkHoney,
374 | darkPink,
375 | darkOrange,
376 | };
377 |
--------------------------------------------------------------------------------
/packages/design-system/src/tokens/radii.ts:
--------------------------------------------------------------------------------
1 | const radiiScale = [2, 4, 8] as const;
2 |
3 | // radii tokens - just demo values - find your right radius
4 | const radii = {
5 | small: radiiScale[0],
6 | medium: radiiScale[1],
7 | large: radiiScale[2],
8 | };
9 |
10 | export { radii };
11 |
--------------------------------------------------------------------------------
/packages/design-system/src/tokens/space.ts:
--------------------------------------------------------------------------------
1 | const spaceScale = [0, 4, 8, 16, 32] as const;
2 |
3 | // space tokens - just demo values - find your right spacing
4 | const space = {
5 | small: spaceScale[1],
6 | medium: spaceScale[2],
7 | large: spaceScale[3],
8 | };
9 |
10 | export { space };
11 |
--------------------------------------------------------------------------------
/packages/design-system/src/tokens/typography.ts:
--------------------------------------------------------------------------------
1 | const fontSizeScale = [14, 16, 18, 20] as const;
2 | const fontWeightScale = [400, 600, 700];
3 |
4 | // font-size tokens - just demo values - find your right font sizes
5 | const fontSizes = {
6 | small: fontSizeScale[0],
7 | base: fontSizeScale[1],
8 | body: fontSizeScale[2],
9 | large: fontSizeScale[3],
10 | };
11 |
12 | // font-weight tokens - just demo values - find your right font weights
13 | const fontWeights = {
14 | regular: fontWeightScale[0],
15 | semibold: fontWeightScale[1],
16 | extrabold: fontWeightScale[2],
17 | };
18 |
19 | // font family tokens - just demo values - find your right font sets
20 | const fontFamily = {
21 | body: 'system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
22 | heading:
23 | '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;',
24 | };
25 |
26 | // line height tokens - just demo values - find your right line heights
27 | const lineHeights = {
28 | normal: 'normal',
29 | none: 1,
30 | base: 1.5,
31 | body: 1.7,
32 | };
33 |
34 | export { fontSizes, fontWeights, fontFamily, lineHeights };
35 |
--------------------------------------------------------------------------------
/packages/design-system/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../core/types';
2 |
--------------------------------------------------------------------------------
/packages/design-system/src/util/__tests__/create-theme.test.ts:
--------------------------------------------------------------------------------
1 | import { createTheme } from '../create-theme';
2 |
3 | describe('util/theme', () => {
4 | it('createTheme', () => {
5 | const themeDefinition = {
6 | name: 'dark',
7 | breakpoints: {
8 | small: 0,
9 | medium: '22em',
10 | },
11 | colors: { primary00: '#aaa', white: 'white' },
12 | space: {
13 | small: 0,
14 | large: 12,
15 | },
16 | };
17 |
18 | const createdTheme = createTheme(themeDefinition);
19 | expect(createdTheme).toEqual({
20 | name: 'dark',
21 | breakpoints: {
22 | small: 'var(--theme-breakpoints-small)',
23 | medium: 'var(--theme-breakpoints-medium)',
24 | },
25 | colors: {
26 | primary00: 'var(--theme-colors-primary00)',
27 | white: 'var(--theme-colors-white)',
28 | },
29 | space: {
30 | small: 'var(--theme-space-small)',
31 | large: 'var(--theme-space-large)',
32 | },
33 |
34 | __definition: {
35 | name: 'dark',
36 | breakpoints: {
37 | small: 0,
38 | medium: '22em',
39 | },
40 | colors: { primary00: '#aaa', white: 'white' },
41 | space: {
42 | small: 0,
43 | large: 12,
44 | },
45 | },
46 |
47 | __cssVars: {
48 | '--theme-breakpoints-small': '0px',
49 | '--theme-breakpoints-medium': '22em',
50 | '--theme-colors-primary00': '#aaa',
51 | '--theme-colors-white': 'white',
52 |
53 | '--theme-space-small': '0px',
54 | '--theme-space-large': '12px',
55 | },
56 | });
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/packages/design-system/src/util/__tests__/object.test.ts:
--------------------------------------------------------------------------------
1 | import { walkObject } from '../object';
2 |
3 | describe('util/object', () => {
4 | it('walkObject', () => {
5 | const theme = {
6 | name: 'dark',
7 | breakpoints: {
8 | small: 0,
9 | medium: '22em',
10 | },
11 | colors: { primary00: '#aaa', white: 'white' },
12 | space: {
13 | small: 0,
14 | large: 12,
15 | },
16 | };
17 |
18 | const results: { value: string | number; location: string[] }[] = [];
19 | walkObject(theme, ({ value, location, isLeaf }) => {
20 | if (isLeaf) results.push({ value, location });
21 | });
22 | expect(results.length).toBe(7);
23 | expect(results[0]).toEqual({ value: 'dark', location: ['name'] });
24 | expect(results[1]).toEqual({
25 | value: 0,
26 | location: ['breakpoints', 'small'],
27 | });
28 | expect(results[2]).toEqual({
29 | value: '22em',
30 | location: ['breakpoints', 'medium'],
31 | });
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/packages/design-system/src/util/create-theme.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import clone from 'just-clone';
3 | import set from 'just-safe-set';
4 | import { walkObject } from './object';
5 |
6 | // restricting token groups to be flat
7 | type TokenGroup = { [key: string]: string | number };
8 | type ThemeDefinition = { [key: string]: string | TokenGroup };
9 |
10 | type CreatedTheme<
11 | T extends ThemeDefinition | TokenGroup,
12 | NestedKey extends string = '--theme-'
13 | > = T extends any
14 | ? {
15 | readonly [Key in keyof T]: T[Key] extends TokenGroup
16 | ? CreatedTheme
17 | : Key extends 'name'
18 | ? T[Key]
19 | : `var(${NestedKey}-${string & Key})`;
20 | }
21 | : never;
22 |
23 | const tokenKeysToSkip = ['name'];
24 | const numericTokenGrouns = ['fontWeights', 'lineHeights'];
25 | export const createTheme = (
26 | themeDefinition: ThemeDef
27 | ): CreatedTheme & {
28 | __definition: ThemeDef;
29 | __cssVars: Record;
30 | } => {
31 | // we store the original tokens in __definition
32 | const __definition = clone(themeDefinition);
33 | // we store the created css variables in __cssVars
34 | const __cssVars: Record = {};
35 | // we assign css vars to token keys
36 | const theme = {} as CreatedTheme;
37 |
38 | // Eg. { value: 'green', location: ['color', 'primary'], isLeaf: true }
39 | walkObject(themeDefinition, ({ value, location, isLeaf }) => {
40 | if (tokenKeysToSkip.includes(location[0])) {
41 | set(theme, location, value);
42 | return;
43 | }
44 | if (isLeaf) {
45 | const cssVarName = `--theme-${location.join('-')}`;
46 | let cssVarValue = value;
47 | if (
48 | typeof value === 'number' &&
49 | !location.some((l) => numericTokenGrouns.includes(l))
50 | ) {
51 | cssVarValue = `${value}px`;
52 | }
53 | set(theme, location, `var(${cssVarName})`);
54 | __cssVars[cssVarName] = cssVarValue;
55 | }
56 | });
57 |
58 | return { ...theme, __definition, __cssVars };
59 | };
60 |
--------------------------------------------------------------------------------
/packages/design-system/src/util/helpers.ts:
--------------------------------------------------------------------------------
1 | export const isBrowser =
2 | typeof window !== 'undefined' && typeof window.document !== 'undefined';
3 |
--------------------------------------------------------------------------------
/packages/design-system/src/util/object.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | export const isObject = (item: unknown) => {
3 | return typeof item === 'object' && !Array.isArray(item) && item !== null;
4 | };
5 |
6 | export const walkObject = (
7 | root: Record,
8 | callback: (arg: {
9 | value: string | number;
10 | key: string;
11 | location: string[];
12 | isLeaf: boolean;
13 | }) => void
14 | ) => {
15 | const walk = (obj: Record, location: string[] = []) => {
16 | Object.keys(obj).forEach((key) => {
17 | // Value is an array, do nothing
18 | if (Array.isArray(obj[key])) {
19 | // do nothing
20 | console.error(
21 | 'Object key with array value detected. Array values will be ignored.',
22 | obj[key]
23 | );
24 | // Value is an object, walk the keys of the object
25 | } else if (isObject(obj[key])) {
26 | callback({
27 | value: obj[key],
28 | key,
29 | location: [...location, ...[key]],
30 | isLeaf: false,
31 | });
32 | walk(obj[key], [...location, ...[key]]);
33 |
34 | // We've reached a leaf node, call fn on the leaf with the location
35 | } else {
36 | callback({
37 | value: obj[key],
38 | key,
39 | location: [...location, ...[key]],
40 | isLeaf: true,
41 | });
42 | }
43 | });
44 | };
45 |
46 | walk(root);
47 | };
48 |
--------------------------------------------------------------------------------
/packages/design-system/tsconfig.jest.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2020",
5 | "module": "commonjs",
6 | "moduleResolution": "node",
7 | "noEmit": true,
8 | "incremental": true,
9 | "noImplicitAny": false,
10 | "declaration": false,
11 | "noUnusedLocals": false,
12 | "noUnusedParameters": false,
13 | "isolatedModules": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/design-system/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": "./src",
5 | "esModuleInterop": true,
6 | "target": "esnext",
7 | "lib": ["dom", "dom.iterable", "esnext"],
8 | "module": "commonjs",
9 | "jsx": "react-jsx",
10 | "jsxImportSource": "@emotion/react",
11 | "incremental": true,
12 | "plugins": [
13 | {
14 | "name": "typescript-styled-plugin"
15 | }
16 | ],
17 | "types": ["jest", "node"]
18 | },
19 | "exclude": ["node_modules", ".yarn", "dist"]
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": false,
4 | "forceConsistentCasingInFileNames": true,
5 | "incremental": true,
6 | "isolatedModules": true,
7 | "module": "commonjs",
8 | "moduleResolution": "node",
9 | "noEmit": true,
10 | "resolveJsonModule": true,
11 | "skipLibCheck": true,
12 | "strict": true,
13 | "useUnknownInCatchVariables": true,
14 | },
15 | "exclude": ["node_modules", ".yarn", "dist"]
16 | }
--------------------------------------------------------------------------------