├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc.js ├── .vscode └── settings.json ├── LICENCE ├── README.md ├── lerna.json ├── package.json ├── packages ├── components │ ├── .eslintignore │ ├── .eslintrc.js │ ├── README.md │ ├── jest-setup.ts │ ├── jest.config.js │ ├── node │ │ └── codemod-prism.ts │ ├── package.json │ ├── src │ │ ├── Box.tsx │ │ ├── Burger.tsx │ │ ├── Button.tsx │ │ ├── CSSConverter.tsx │ │ ├── Cart.tsx │ │ ├── Code │ │ │ ├── Code.tsx │ │ │ ├── __mocks__ │ │ │ │ └── tokens.ts │ │ │ ├── __prism │ │ │ │ └── index.ts │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── convertor.test.ts.snap │ │ │ │ │ └── normalizer.test.ts.snap │ │ │ │ ├── code.test.tsx │ │ │ │ ├── convertor.test.ts │ │ │ │ └── normalizer.test.ts │ │ │ ├── convertor.ts │ │ │ ├── index.ts │ │ │ └── normalizer.ts │ │ ├── Column.tsx │ │ ├── Columns.tsx │ │ ├── Heading.tsx │ │ ├── Layer.tsx │ │ ├── LayerAside.tsx │ │ ├── LayerShim.tsx │ │ ├── Legend.tsx │ │ ├── Link.tsx │ │ ├── Select │ │ │ ├── Frame.tsx │ │ │ ├── Item.tsx │ │ │ ├── Select.test.tsx │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── Small.tsx │ │ ├── Stack.tsx │ │ ├── Table │ │ │ ├── Cell.tsx │ │ │ ├── Frame.tsx │ │ │ ├── Table.test.tsx │ │ │ └── index.ts │ │ ├── Text.tsx │ │ ├── Video │ │ │ ├── Controls.tsx │ │ │ ├── Stage.tsx │ │ │ ├── Video.tsx │ │ │ ├── __tests__ │ │ │ │ └── Video.test.tsx │ │ │ └── index.ts │ │ ├── __tests__ │ │ │ ├── Box.test.tsx │ │ │ ├── Button.test.tsx │ │ │ ├── CSSConverter.test.tsx │ │ │ ├── Cart.test.tsx │ │ │ ├── Column.test.tsx │ │ │ ├── Columns.test.tsx │ │ │ ├── Heading.test.tsx │ │ │ ├── Layer.test.tsx │ │ │ ├── LayerAside.test.tsx │ │ │ ├── Legend.test.tsx │ │ │ ├── Link.test.tsx │ │ │ ├── Small.test.tsx │ │ │ ├── Stack.test.tsx │ │ │ └── Text.test.tsx │ │ ├── index.ts │ │ ├── styles.ts │ │ ├── theme │ │ │ ├── __tests__ │ │ │ │ ├── color.test.tsx │ │ │ │ ├── scale.test.tsx │ │ │ │ └── variant.test.tsx │ │ │ ├── code-theme.ts │ │ │ ├── color.ts │ │ │ ├── index.ts │ │ │ ├── scale.ts │ │ │ ├── theme.ts │ │ │ ├── types.ts │ │ │ └── variant.ts │ │ └── types │ │ │ ├── emotion.d.ts │ │ │ └── globals.d.ts │ ├── testing-library.tsx │ └── tsconfig.json └── hooks │ ├── .eslintrc.js │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── use-carousel.test.ts.snap │ │ │ └── use-intersection-observer.test.tsx.snap │ │ ├── use-carousel.test.ts │ │ ├── use-intersection-observer.test.tsx │ │ ├── use-media.test.ts │ │ └── use-video-control.test.tsx │ ├── index.ts │ ├── use-carousel.ts │ ├── use-intersection-observer.ts │ ├── use-media.ts │ └── use-video-control.ts │ ├── testing-library-setup.ts │ └── tsconfig.json ├── renovate.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | plugins: ["react-hooks", "@emotion"], 4 | extends: [ 5 | "plugin:react/recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "prettier", 8 | ], 9 | parserOptions: { 10 | ecmaVersion: 2018, 11 | sourceType: "module", 12 | ecmaFeatures: { 13 | jsx: true, 14 | }, 15 | }, 16 | rules: { 17 | "react-hooks/rules-of-hooks": "error", 18 | "no-unused-vars": "off", 19 | "@typescript-eslint/no-unused-vars": [ 20 | "error", 21 | { 22 | vars: "all", 23 | args: "after-used", 24 | ignoreRestSiblings: false, 25 | }, 26 | ], 27 | "react/prop-types": "off", 28 | "@typescript-eslint/no-var-requires": "off", 29 | "@typescript-eslint/explicit-module-boundary-types": [ 30 | "off", 31 | { allowExpressions: true }, 32 | ], 33 | "@typescript-eslint/explicit-function-return-type": [ 34 | "off", 35 | { allowExpressions: true }, 36 | ], 37 | }, 38 | settings: { 39 | react: { 40 | version: "detect", 41 | }, 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the master branch 5 | on: 6 | push: 7 | branches: 8 | - "**" 9 | 10 | jobs: 11 | # This workflow contains a single job called "build" 12 | build: 13 | # The type of runner that the job will run on 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node: ["12"] 18 | name: Node version ${{ matrix.node }} 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - uses: actions/checkout@v2 24 | 25 | - name: Setup node 26 | uses: actions/setup-node@v2-beta 27 | 28 | # Runs a single command using the runners shell 29 | - name: Yarn Install 30 | run: yarn 31 | 32 | - name: Lint 33 | run: yarn lint 34 | 35 | - name: Test 36 | run: yarn test 37 | 38 | - name: Build 39 | run: | 40 | yarn build:components 41 | yarn build:hooks 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | .docz 5 | .cache 6 | .DS_Store 7 | lib 8 | yarn-error.log 9 | public 10 | .env 11 | .env.* 12 | packages/*/tsconfig.tsbuildinfo 13 | .vercel -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: "avoid", 3 | bracketSpacing: true, 4 | jsxBracketSameLine: true, 5 | printWidth: 80, 6 | trailingComma: "all", 7 | tabWidth: 2, 8 | semi: false, 9 | singleQuote: false, 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "/Users/mau/projects/wdlk/node_modules/typescript/lib", 3 | "prettier.packageManager": "yarn", 4 | "prettier.singleQuote": true, 5 | "editor.formatOnSave": true, 6 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" 7 | } 8 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Woodlike 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @WDLK 2 | 3 | 4 |
5 | Woodlike Logo 6 |
7 | 8 | [![Actions Status](https://github.com/woodlike/wdlk/workflows/CI/badge.svg)](https://github.com/woodlike/wdlk/actions) 9 | 10 | ## Motivation 11 | 12 | This is Woodlike's multi-package repository containing the latest JavaScript modules we crafted for our own business operations. At Woodlike we craft swimwear out of ocean recovered fishing nets. We are committed to creating healthy oceans and being part of positive change. 13 | 14 | Deep in our hearts, we are makers, designers, technologist and believe very much in making our work public for others to use, improve or play around. 15 | 16 | ### Development Requirements 17 | 18 | 🚀 Node.js >=10 19 | 🌲 Git 20 | 🐈 yarn >= 1.12 21 | 22 | ### Getting started with development 23 | 24 | ```sh 25 | git clone git@github.com:woodlike/wdlk.git 26 | cd wdlk 27 | # Install packages 28 | yarn 29 | ``` 30 | 31 | ### Commands 32 | 33 | The list of most important commands to work with the selected workspace. 34 | 35 | | Commands | Package | Description | 36 | | ------------------------ | -------------------- | ------------------------------------------------------ | 37 | | `yarn dev` | **@wdlk/\*\*** | Lerna executes the dev command on all the packages | 38 | | `yarn build` | **@wdlk/\*\*** | Lerna executes the build command on all the packages | 39 | | `yarn lint` | **@wdlk/\*\*** | Lerna executes the lint command on all the packages | 40 | | `yarn test` | **@wdlk/\*\*** | Lerna executes the test command on all the packages | 41 | | `yarn build:components` | **@wdlk/components** | compile Typescript into JavaScript | 42 | | `yarn build:containers` | **@wdlk/containers** | compile Typescript into JavaScript | 43 | | `yarn build:theme-query` | **theme-query** | compile Typescript into JavaScript | 44 | | `yarn dev:components` | **@wdlk/components** | compile Typescript into JavaScript in watch mode | 45 | | `yarn dev:containers` | **@wdlk/containers** | compile Typescript into JavaScript in watch mode | 46 | | `yarn lint:components` | **@wdlk/components** | Lint the package according to the Eslint configuration | 47 | | `yarn lint:containers` | **@wdlk/containers** | Lint the package according to the Eslint configuration | 48 | | `yarn lint:theme-query` | **theme-query** | Lint the package according to the Eslint configuration | 49 | | `yarn test:components` | **@wdlk/components** | Run unit tests written with Jest | 50 | | `yarn test:containers` | **@wdlk/containers** | Run unit tests written with Jest | 51 | | `yarn test:theme-query` | **theme-query** | Run unit tests written with Jest | 52 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "version": "independent" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wdlk", 3 | "version": "1.0.1", 4 | "private": true, 5 | "description": "WDLK multi package repo", 6 | "workspaces": { 7 | "packages": [ 8 | "packages/*" 9 | ], 10 | "nohoist": [ 11 | "**/prismjs", 12 | "**/ts-node", 13 | "**/@types/node" 14 | ] 15 | }, 16 | "scripts": { 17 | "dev": "lerna run dev --parallel", 18 | "clean": "lerna run clean --parallel", 19 | "lint": "lerna run lint --parallel", 20 | "test": "lerna run test --parallel", 21 | "build": "lerna run build --parallel", 22 | "build:components": "yarn workspace @wdlk/components build", 23 | "build:hooks": "yarn workspace @wdlk/hooks build", 24 | "store:build": "yarn workspace @wdlk/store store:build", 25 | "build:theme-query": "yarn workspace theme-query build", 26 | "clean:store": "yarn workspace @wdlk/store clean", 27 | "dev:components": "yarn workspace @wdlk/components dev", 28 | "dev:hooks": "yarn workspace @wdlk/hooks dev", 29 | "store": "yarn workspace @wdlk/store store", 30 | "dev:theme-query": "yarn workspace theme-query dev", 31 | "lint:components": "yarn workspace @wdlk/components lint", 32 | "lint:hooks": "yarn workspace @wdlk/hooks lint", 33 | "lint:store": "yarn workspace @wdlk/store lint", 34 | "lint:theme-query": "yarn workspace theme-query lint", 35 | "prism:components": "yarn workspace @wdlk/components codemod:prism", 36 | "test:components": "yarn workspace @wdlk/components test", 37 | "test:hooks": "yarn workspace @wdlk/hooks test", 38 | "test:store": "yarn workspace @wdlk/store test", 39 | "test:theme-query": "yarn workspace theme-query test", 40 | "type-check:store": "yarn workspace @wdlk/store type-check" 41 | }, 42 | "devDependencies": { 43 | "@emotion/eslint-plugin": "^11.7.0", 44 | "@typescript-eslint/eslint-plugin": "^5.26.0", 45 | "@typescript-eslint/parser": "^5.26.0", 46 | "eslint": "8.16.0", 47 | "eslint-config-prettier": "^8.5.0", 48 | "eslint-plugin-node": "^11.1.0", 49 | "eslint-plugin-prettier": "^4.0.0", 50 | "eslint-plugin-promise": "^6.0.0", 51 | "eslint-plugin-react": "7.30.0", 52 | "eslint-plugin-react-hooks": "^4.5.0", 53 | "lerna": "^3.22.1", 54 | "typescript": "4.6" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/components/.eslintignore: -------------------------------------------------------------------------------- 1 | src/code/__prism/** 2 | -------------------------------------------------------------------------------- /packages/components/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("../../.eslintrc.js"), 3 | rules: { 4 | "@emotion/jsx-import": "error", 5 | "@emotion/pkg-renaming": "error", 6 | "@typescript-eslint/no-use-before-define": "off", 7 | "@emotion/jsx-import": "error", 8 | "@emotion/pkg-renaming": "error", 9 | "@typescript-eslint/member-delimiter-style": [ 10 | "error", 11 | { 12 | multiline: { 13 | delimiter: "none", 14 | requireLast: true, 15 | }, 16 | }, 17 | ], 18 | "@typescript-eslint/explicit-function-return-type": [ 19 | "warn", 20 | { allowExpressions: true }, 21 | ], 22 | "react/prop-types": "off", 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /packages/components/README.md: -------------------------------------------------------------------------------- 1 | # @wdlk/components 2 | 3 | [![CircleCI](https://circleci.com/gh/woodlike/wdlk.svg?style=svg)](https://circleci.com/gh/woodlike/wdlk) 4 | 5 | ## Requirements 6 | 7 | 🚀 Node.js >=8 8 | 🌲 Git 9 | 🐈 yarn >= 1.12 10 | 11 | ## Motivation 12 | 13 | Are reusable stateless functional components that are coherent to a design system. It uses [Theme-UI](https://theme-ui.com/) under the hood to provide a design system constrained development. With this approach, the theme object becomes the heart of a user interface identity without setting constraints on layout and design decisions. 14 | 15 | ## Getting started 16 | 17 | ```sh 18 | yarn add @wdk/components 19 | ``` 20 | -------------------------------------------------------------------------------- /packages/components/jest-setup.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable: no-import-side-effect 2 | import "jest-axe/extend-expect" 3 | import "@testing-library/jest-dom/extend-expect" 4 | 5 | import { matchers } from "@emotion/jest" 6 | 7 | beforeEach(() => { 8 | expect.extend(matchers) 9 | }) 10 | -------------------------------------------------------------------------------- /packages/components/jest.config.js: -------------------------------------------------------------------------------- 1 | const TEST_REGEX = "(/__tests__/.*|(\\.|/)(test|spec))\\.(tsx?|ts?)$" 2 | 3 | module.exports = { 4 | globals: { 5 | "ts-jest": { 6 | isolatedModules: true, 7 | }, 8 | }, 9 | preset: "ts-jest", 10 | moduleFileExtensions: ["ts", "tsx", "js", "json"], 11 | testEnvironment: "jsdom", 12 | transform: { 13 | ".(ts|tsx)": "ts-jest", 14 | }, 15 | setupFilesAfterEnv: ["./jest-setup.ts"], 16 | // snapshotSerializers: ["@emotion/jest"], 17 | testRegex: TEST_REGEX, 18 | testPathIgnorePatterns: ["/node_modules/", "/lib"], 19 | } 20 | -------------------------------------------------------------------------------- /packages/components/node/codemod-prism.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | import { File, ImportDeclaration, ExportDefaultDeclaration, Program } from '@babel/types'; 4 | 5 | const { default: generate } = require('@babel/generator'); 6 | const { dirname, resolve } = require('path'); 7 | const { parse } = require('@babel/parser'); 8 | const { readdirSync, readFileSync, writeFile } = require('fs'); 9 | const prettier = require('prettier'); 10 | const t = require('@babel/types'); 11 | 12 | const pathToPrism = dirname(require.resolve('prismjs')); 13 | const languages = [ 14 | 'markup', 15 | 'bash', 16 | 'css', 17 | 'css-extras', 18 | 'clike', 19 | 'javascript', 20 | 'jsx', 21 | 'js-extras', 22 | 'git', 23 | 'graphql', 24 | 'json', 25 | 'ocaml', 26 | 'reason', 27 | 'tsx', 28 | 'typescript', 29 | 'yaml', 30 | ]; 31 | const path = `${pathToPrism}/components`; 32 | const outputPath = resolve('./src', 'code/__prism/', 'index.ts'); 33 | 34 | const introFileComment = 35 | 'This is an auto-generated file to override the default Prism languages. Check the node directory to see the implementation or run the prism:lang command on this package to generate a fresh set of languages.'; 36 | 37 | const typeCheckComment = ` 38 | //@ts-nocheck 39 | /* eslint no-var: 0 */ 40 | /* eslint @typescript-eslint/camelcase: 0 */ 41 | /* eslint @typescript-eslint/explicit-function-return-type: 0 */ 42 | `; 43 | 44 | function createImportNode(name: string, vendor: string): ImportDeclaration { 45 | return t.importDeclaration([t.importNamespaceSpecifier(t.identifier(name))], t.stringLiteral(vendor)); 46 | } 47 | 48 | function createExportNode(name: string): ExportDefaultDeclaration { 49 | return t.exportDefaultDeclaration(t.identifier(name)); 50 | } 51 | 52 | function getLanguages(path: string, langs: string[]): string[] { 53 | const files = readdirSync(path, 'utf-8'); 54 | return langs.map(lang => files.find((f: string) => f === `prism-${lang}.js`)); 55 | } 56 | 57 | function parseLangs(path: string, files: string[]): File { 58 | const source = files.map(file => readFileSync(`${path}/${file}`, 'utf-8')).join(' '); 59 | return parse(`${source}`); 60 | } 61 | 62 | function generateVendorCode(ast: File): string { 63 | const importNode = createImportNode('Prism', 'prismjs'); 64 | const exportNode = createExportNode('Prism'); 65 | const config = { 66 | bracketSpacing: true, 67 | parser: 'babel', 68 | printWidth: 120, 69 | semi: true, 70 | singleQuote: true, 71 | tabWidth: 2, 72 | trailingComma: 'all', 73 | }; 74 | 75 | const newAst: Partial = { 76 | type: 'Program', 77 | body: [importNode, ...ast.program.body, exportNode], 78 | }; 79 | 80 | const { code } = generate(newAst, { auxiliaryCommentBefore: introFileComment }); 81 | return prettier.format(typeCheckComment.concat(code), config); 82 | } 83 | 84 | function generateFile(data: string, path: string): void { 85 | try { 86 | writeFile(path, data, 'utf-8', (err: Error) => { 87 | if (err) { 88 | throw new Error(`Error writting the file file: ${err}`); 89 | } 90 | console.log('Your Prism file was successfully written!'); 91 | }); 92 | } catch (error) { 93 | console.error(error); 94 | } 95 | } 96 | 97 | generateFile(generateVendorCode(parseLangs(path, getLanguages(path, languages))), outputPath); 98 | -------------------------------------------------------------------------------- /packages/components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wdlk/components", 3 | "version": "6.1.4", 4 | "description": "Stateless functional components that are coherent to a design system", 5 | "repository": "git@github.com:woodlike/component-library.git", 6 | "license": "MIT", 7 | "author": "Mauricio Palma ", 8 | "main": "lib/index.js", 9 | "scripts": { 10 | "build": "tsc --module commonjs --target es6 --outDir lib/", 11 | "dev": "tsc --watch --target es6 --outDir lib/", 12 | "lint": "eslint src --ext .ts,.tsx", 13 | "codemod:prism": "ts-node node/codemod-prism.ts", 14 | "test": "jest", 15 | "test:coverage": "npm test -- --coverage" 16 | }, 17 | "dependencies": { 18 | "prismjs": "^1.22.0", 19 | "shortid": "^2.2.15" 20 | }, 21 | "devDependencies": { 22 | "@babel/generator": "^7.8.8", 23 | "@babel/parser": "^7.8.8", 24 | "@babel/types": "^7.8.7", 25 | "@emotion/eslint-plugin": "^11.7.0", 26 | "@emotion/jest": "^11.9.1", 27 | "@emotion/react": "^11.9.0", 28 | "@emotion/styled": "^11.8.1", 29 | "@mdx-js/react": "^2.1.1", 30 | "@testing-library/jest-dom": "^5.16.4", 31 | "@testing-library/react-hooks": "^8.0.0", 32 | "@testing-library/react": "^13.2.0", 33 | "@types/babel__parser": "^7.1.1", 34 | "@types/jest-axe": "^3.2.1", 35 | "@types/jest": "^24.0.23", 36 | "@types/node": "^13.9.1", 37 | "@types/prismjs": "^1.16.0", 38 | "@types/react": "^18.0.9", 39 | "@types/react-dom": "^18.0.5", 40 | "@types/testing-library__dom": "^7.5.0", 41 | "jest-axe": "^3.2.0", 42 | "jest": "^24.9.0", 43 | "prettier": "^1.19.1", 44 | "react-dom": "^18.1.0", 45 | "react": "^18.1.0", 46 | "ts-jest": "^24.1.0", 47 | "ts-node": "^8.6.2", 48 | "typescript": "4.6" 49 | }, 50 | "peerDependencies": { 51 | "@emotion/react": "^11.9.0", 52 | "@emotion/styled": "^11.8.1", 53 | "react": "^18.1.0", 54 | "react-dom": "^18.1.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/components/src/Box.tsx: -------------------------------------------------------------------------------- 1 | import { Color, Scale, ScaleArea } from "." 2 | 3 | import React from "react" 4 | import styled from "@emotion/styled" 5 | 6 | export interface BoxProps { 7 | readonly padding: ScaleArea 8 | readonly breakpoint?: number 9 | readonly maxPadding?: ScaleArea 10 | readonly as?: BoxHTMLElement 11 | readonly borderWidths?: ScaleArea 12 | readonly borderStyles?: ScaleArea 13 | readonly borderColors?: BorderColorProps 14 | readonly backgroundColor?: string | BorderColorScale 15 | readonly className?: string 16 | } 17 | 18 | interface StyledBoxProps { 19 | readonly padding: ScaleArea 20 | readonly breakpoint: number 21 | readonly maxPadding?: ScaleArea 22 | readonly borderWidths?: ScaleArea 23 | readonly borderStyles?: ScaleArea 24 | readonly borderColors?: BorderColorProps 25 | readonly backgroundColor?: string | BorderColorScale 26 | } 27 | 28 | export interface BorderColorScale { 29 | readonly color: string 30 | readonly idx: number 31 | } 32 | 33 | export type BoxHTMLElement = 34 | | "div" 35 | | "span" 36 | | "section" 37 | | "article" 38 | | "main" 39 | | "header" 40 | | "footer" 41 | | "figure" 42 | | "nav" 43 | | "ul" 44 | | "li" 45 | | "aside" 46 | 47 | export type BorderColorProps = 48 | | (string | BorderColorScale) 49 | | (string | BorderColorScale)[] 50 | 51 | const StyledBox = styled.div` 52 | padding: ${({ padding, theme }) => 53 | Scale.toCSSPixel(Scale.create(padding, theme.space))}; 54 | border-width: ${props => 55 | props.borderWidths && 56 | Scale.toCSSPixel( 57 | Scale.create(props.borderWidths as ScaleArea, props.theme.borderWidths), 58 | )}; 59 | border-style: ${({ borderStyles, theme }) => 60 | borderStyles && 61 | Scale.toCSSString( 62 | Scale.create(borderStyles as ScaleArea, theme.borderStyles), 63 | )}; 64 | border-color: ${props => { 65 | const { borderColors, theme } = props 66 | if (Array.isArray(borderColors)) { 67 | const colors = borderColors.map(color => { 68 | return typeof color === "object" 69 | ? Color.query(color.color, theme.colors, color.idx) 70 | : Color.query(color, theme.colors) 71 | }) as string[] 72 | 73 | return Scale.createBox(colors).join(" ") 74 | } 75 | 76 | return typeof borderColors === "object" 77 | ? Color.query(borderColors.color, theme.colors, borderColors.idx) 78 | : Color.query(borderColors as string, theme.colors) 79 | }}; 80 | background-color: ${({ backgroundColor, theme }) => 81 | typeof backgroundColor === "object" 82 | ? Color.query(backgroundColor.color, theme.colors, backgroundColor.idx) 83 | : Color.query(backgroundColor as string, theme.colors)}; 84 | margin: 0; 85 | list-style: ${props => props.as === "ul" && "none"}; 86 | 87 | @media (min-width: ${props => props.theme.breakpoints[props.breakpoint]}) { 88 | padding: ${props => { 89 | const { maxPadding, padding, theme } = props 90 | if (!maxPadding) { 91 | return Scale.toCSSPixel(Scale.create(padding, theme.space)) 92 | } 93 | 94 | return Scale.toCSSPixel(Scale.create(maxPadding, theme.space)) 95 | }}; 96 | } 97 | ` 98 | 99 | StyledBox.displayName = "StyledBox" 100 | 101 | export const Box = (props: React.PropsWithChildren): JSX.Element => ( 102 | 111 | {props.children} 112 | 113 | ) 114 | -------------------------------------------------------------------------------- /packages/components/src/Burger.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { css } from "@emotion/react" 3 | 4 | import styled from "@emotion/styled" 5 | 6 | export interface BurgerProps { 7 | readonly isActive: boolean 8 | readonly onClick: React.MouseEventHandler 9 | } 10 | 11 | export interface StyledBurgerProps { 12 | readonly isActive: boolean 13 | } 14 | 15 | const HEIGHT = 28 16 | const WIDTH = 35 17 | 18 | const StyledBurger = styled.div` 19 | position: relative; 20 | width: ${WIDTH}px; 21 | height: ${HEIGHT}px; 22 | cursor: pointer; 23 | ` 24 | 25 | StyledBurger.displayName = "StyledBurger" 26 | 27 | const stylesLine = css` 28 | position: absolute; 29 | top: 50%; 30 | left: 0; 31 | display: inline-block; 32 | width: 100%; 33 | height: 1px; 34 | background-color: black; 35 | transform-origin: center; 36 | transition: transform 250ms ease-in-out; 37 | ` 38 | 39 | const StyledTopLine = styled.span` 40 | ${stylesLine} 41 | transform: ${({ isActive }) => 42 | isActive 43 | ? css`translate3d(0, -50%, 0) rotate(45deg)` 44 | : css`translate3d(0, ${Math.round( 45 | (HEIGHT / 3) * -1, 46 | )}px, 0) rotate(0deg)`}; 47 | ` 48 | 49 | StyledTopLine.displayName = "StyledTopLine" 50 | 51 | const StyledMidLine = styled.span` 52 | ${stylesLine} 53 | opacity: ${({ isActive }) => (isActive ? 0 : 1)}; 54 | ` 55 | 56 | StyledMidLine.displayName = "StyledMidLine" 57 | 58 | const StyledBottomLine = styled.span` 59 | ${stylesLine} 60 | transform: ${({ isActive }) => 61 | isActive 62 | ? css`translate3d(0, -50%, 0) rotate(-45deg)` 63 | : css`translate3d(0, ${Math.round(HEIGHT / 3)}px, 0) rotate(0deg)`}; 64 | ` 65 | 66 | StyledBottomLine.displayName = "StyledBottomLine" 67 | 68 | export const Burger = (props: React.PropsWithChildren): JSX.Element => ( 69 | 70 | 71 | 72 | 73 | 74 | ) 75 | -------------------------------------------------------------------------------- /packages/components/src/Button.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonVariant, Scale, ScaleArea } from "." 2 | import { SerializedStyles, css } from "@emotion/react" 3 | 4 | import React from "react" 5 | import styled from "@emotion/styled" 6 | 7 | export const BUTTON_LOADING_Y_KEY = "--wdlk-button-animation-y" 8 | 9 | export interface ButtonProps { 10 | variant: ButtonVariantType 11 | onClick: React.MouseEventHandler 12 | disabled?: boolean 13 | isLoading?: boolean 14 | padding?: ScaleArea 15 | loadingCounter?: number 16 | } 17 | 18 | type ButtonVariantType = "primary" | "secondary" 19 | 20 | const stylesPseudoElements = css` 21 | content: ""; 22 | position: absolute; 23 | bottom: 0; 24 | right: 0; 25 | z-index: -1; 26 | width: 100%; 27 | height: 100%; 28 | ` 29 | 30 | const getHoverStyles = (disabled: boolean): SerializedStyles => { 31 | if (disabled) return css`` 32 | return css` 33 | :hover { 34 | &::before { 35 | opacity: 0.8; 36 | transition: opacity 200ms ease-in-out; 37 | } 38 | } 39 | ` 40 | } 41 | 42 | interface StylesVariantOptions { 43 | variant: ButtonVariant 44 | disabled?: boolean 45 | isLoading?: boolean 46 | } 47 | 48 | const getVariantStyles = ({ 49 | variant, 50 | disabled = false, 51 | isLoading = false, 52 | }: StylesVariantOptions): SerializedStyles => { 53 | const bgColor = disabled ? variant.disabled : variant.bg 54 | const borderColor = disabled ? variant.disabled : variant.color 55 | 56 | return css` 57 | color: ${variant.color}; 58 | border-color: ${borderColor}; 59 | 60 | ::before { 61 | ${stylesPseudoElements} 62 | background-color: ${isLoading ? variant.disabled : bgColor}; 63 | } 64 | 65 | ::after { 66 | ${stylesPseudoElements} 67 | background-color: ${variant.bg}; 68 | pointer-events: none; 69 | transform: translate3d(0, calc(var(${BUTTON_LOADING_Y_KEY}, 100) * 1%), 0); 70 | transition: transform 500ms ease-out; 71 | } 72 | 73 | ${!isLoading ? getHoverStyles(disabled) : ""} 74 | ` 75 | } 76 | 77 | interface StyledButtonProps { 78 | variant: ButtonVariantType 79 | disabled?: boolean 80 | isLoading?: boolean 81 | padding?: ScaleArea 82 | } 83 | 84 | const StyledButton = styled.button` 85 | position: relative; 86 | width: 100%; 87 | padding: ${({ padding, theme }) => { 88 | return Scale.toCSSPixel(Scale.create(padding ?? 1, theme.space)) 89 | }}; 90 | 91 | border-width: 2px; 92 | border-style: solid; 93 | letter-spacing: 2px; 94 | line-height: 1; 95 | font-size: ${({ theme }) => `${theme.fontSizes[3]}px`}; 96 | text-align: center; 97 | text-transform: uppercase; 98 | overflow: hidden; 99 | background: transparent; 100 | cursor: ${({ disabled, isLoading }) => { 101 | if (!!disabled) return "not-allowed" 102 | if (!!isLoading) return "wait" 103 | return "pointer" 104 | }}; 105 | 106 | ${({ disabled, isLoading, theme, variant }) => { 107 | const buttons = theme.buttons 108 | const button = buttons[variant] 109 | 110 | return getVariantStyles({ disabled, isLoading, variant: button }) 111 | }}; 112 | ` 113 | 114 | StyledButton.displayName = "StyledButton" 115 | 116 | export const Button = ({ 117 | children, 118 | disabled, 119 | isLoading, 120 | loadingCounter, 121 | onClick, 122 | padding, 123 | variant, 124 | }: React.PropsWithChildren): JSX.Element => { 125 | const buttonRef = React.useRef(null) 126 | 127 | React.useEffect(() => { 128 | if (!buttonRef.current) return 129 | const style = buttonRef.current.style 130 | 131 | if (loadingCounter === undefined) { 132 | style.setProperty(BUTTON_LOADING_Y_KEY, "100") 133 | return 134 | } 135 | 136 | style.setProperty(BUTTON_LOADING_Y_KEY, `${loadingCounter}`) 137 | }, [isLoading, loadingCounter]) 138 | 139 | return ( 140 | 147 | {children} 148 | 149 | ) 150 | } 151 | -------------------------------------------------------------------------------- /packages/components/src/CSSConverter.tsx: -------------------------------------------------------------------------------- 1 | import React, { ElementType } from "react" 2 | 3 | import { PrismStyleProp } from "." 4 | import styled from "@emotion/styled" 5 | import type { TokenStream } from "prismjs" 6 | 7 | export const CSS_CONTAINER_TEST_ID = "css-container-test-id" 8 | 9 | export interface CSSConverterProps { 10 | readonly cssObject: PrismStyleProp | undefined 11 | readonly tokenStream?: TokenStream 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | readonly as?: (ElementType | undefined) & string 14 | } 15 | 16 | function toCSSString(cssObject: PrismStyleProp | undefined): string { 17 | return !!cssObject 18 | ? Object.entries(cssObject) 19 | .reduce( 20 | (acc: string[], [key, val]) => [ 21 | ...acc, 22 | `${key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()}: ${val};`, 23 | ], 24 | [], 25 | ) 26 | .join("") 27 | : "" 28 | } 29 | 30 | const StyledCSSContainer = styled.span` 31 | ${props => toCSSString(props.cssObject)}; 32 | ` 33 | 34 | StyledCSSContainer.displayName = "StyledCSSContainer" 35 | 36 | export const CSSConverter: React.FC = props => ( 37 | 42 | ) 43 | -------------------------------------------------------------------------------- /packages/components/src/Cart.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import styled from "@emotion/styled" 3 | 4 | export interface CartProps { 5 | readonly isFocused: boolean 6 | readonly isFilled: boolean 7 | readonly title: string 8 | readonly count: number | undefined 9 | readonly onClick: React.MouseEventHandler 10 | } 11 | 12 | interface StyledCartProps { 13 | readonly isFilled: boolean 14 | } 15 | 16 | const StyledCart = styled.svg` 17 | width: ${props => props.theme.cart.width}px; 18 | height ${props => props.theme.cart.height}px; 19 | color: ${props => props.theme.cart.color}; 20 | fill: currentColor; 21 | stroke: currentColor; 22 | stroke-width: 2px; 23 | transition-property: color; 24 | transition-duration: ${props => props.theme.transition.duration}; 25 | transition-timing-function: ${props => props.theme.transition.timing}; 26 | fill-opacity: ${props => (props.isFilled ? 1 : 0)}; 27 | cursor: pointer; 28 | 29 | :hover { 30 | color: ${props => props.theme.cart.hover}; 31 | } 32 | ` 33 | 34 | StyledCart.displayName = "StyledCart" 35 | 36 | const StyledBagLabel = styled.text` 37 | font-family: ${props => props.theme.fonts.heading.display}; 38 | font-weight: ${props => props.theme.fontWeights[5]}; 39 | color: ${props => props.theme.colors.background}; 40 | letter-spacing: ${props => props.theme.letterSpacings[2]}px; 41 | stroke-width: 0; 42 | fill: currentColor; 43 | ` 44 | 45 | StyledBagLabel.displayName = "StyledBagLabel" 46 | 47 | export const Cart: React.FC = (props): JSX.Element => { 48 | return ( 49 | 55 | {props.title} 56 | The shopping bag currently contains {props.count} items 57 | 58 | 62 | 63 | {props.count} 64 | 65 | 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /packages/components/src/Code/Code.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | 3 | import * as Prism from "./__prism" 4 | 5 | import React, { useMemo } from "react" 6 | import { Theme, jsx, useTheme } from "@emotion/react" 7 | import { convertor, normalizer } from "." 8 | 9 | import { CSSConverter } from ".." 10 | import type { Token } from "prismjs" 11 | import styled from "@emotion/styled" 12 | 13 | export interface CodeProps { 14 | readonly code: string 15 | readonly size: "s" | "m" | "l" 16 | readonly lang: Language 17 | readonly theme?: PrismTheme 18 | } 19 | 20 | export interface PrismTheme { 21 | readonly plain: PrismStyleProp 22 | readonly styles: PrismStyleRule[] 23 | } 24 | 25 | export interface PrismStyleRule { 26 | readonly types: string[] 27 | readonly style: PrismStyleProp 28 | } 29 | 30 | export interface PrismStyleProp { 31 | readonly color: string 32 | readonly backgroundColor?: string 33 | readonly fontStyle?: "normal" | "italic" 34 | readonly fontWeight?: 35 | | "normal" 36 | | "bold" 37 | | "100" 38 | | "200" 39 | | "300" 40 | | "400" 41 | | "500" 42 | | "600" 43 | | "700" 44 | | "800" 45 | | "900" 46 | readonly textDecorationLine?: 47 | | "none" 48 | | "underline" 49 | | "line-through" 50 | | "underline line-through" 51 | readonly opacity?: number 52 | readonly [styleKey: string]: string | number | void 53 | } 54 | 55 | interface StyledPreProps { 56 | readonly size: "s" | "m" | "l" 57 | } 58 | 59 | export enum Language { 60 | markup = "markup", 61 | bash = "bash", 62 | css = "css", 63 | cssExtras = "css-extras", 64 | clike = "clike", 65 | javascript = "javascript", 66 | jsx = "jsx", 67 | jsExtras = "js-extras", 68 | git = "git", 69 | graphql = "graphql", 70 | json = "json", 71 | ocaml = "ocaml", 72 | reason = "reason", 73 | tsx = "tsx", 74 | typescript = "typescript", 75 | yaml = "yaml", 76 | } 77 | 78 | const StyledPre = styled.pre` 79 | width: 100%; 80 | padding: ${props => props.theme.space[4]}px; 81 | border-radius: 9px; 82 | margin: 0; 83 | font-family: ${props => props.theme.fonts.monospace}; 84 | font-size: ${props => { 85 | const { size, theme } = props 86 | return !!theme.code[size] ? `${theme.code[size].fontSize}px` : "" 87 | }}; 88 | white-space: pre-wrap; 89 | word-break: break-word; 90 | text-align: left; 91 | color: ${props => props.theme.code.theme.plain.color}; 92 | background-color: ${props => props.theme.code.theme.plain.backgroundColor}; 93 | ` 94 | 95 | StyledPre.displayName = "StyledPre" 96 | 97 | const StyledCode = styled.code` 98 | font-family: inherit; 99 | ` 100 | 101 | StyledCode.displayName = "StyledCode" 102 | 103 | export function handleTokens(code: string, langs: Language): Token[] { 104 | const { languages, tokenize } = Prism.default 105 | const grammar = languages[langs] 106 | 107 | return normalizer(tokenize(code, grammar)) 108 | } 109 | 110 | export const Code: React.FC = props => { 111 | const { code } = useTheme() as Theme 112 | const tokens = handleTokens(props.code, props.lang) 113 | const theme = convertor(props.theme || code.theme) 114 | 115 | const MemoTokens = useMemo(() => { 116 | return ( 117 | 118 | {tokens.map((token, i) => ( 119 | 122 | ))} 123 | 124 | ) 125 | }, [tokens]) 126 | 127 | return ( 128 | 129 | {MemoTokens} 130 | 131 | ) 132 | } 133 | -------------------------------------------------------------------------------- /packages/components/src/Code/__tests__/__snapshots__/convertor.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`convertor() Create theme map should match the snapshot map 1`] = ` 4 | Map { 5 | "comment" => Object { 6 | "color": "rgb(95, 97, 103)", 7 | }, 8 | "keyword" => Object { 9 | "color": "rgb(199, 77, 238)", 10 | }, 11 | "punctuation" => Object { 12 | "color": "rgb(238, 238, 238)", 13 | }, 14 | "arrow" => Object { 15 | "color": "rgb(238, 238, 238)", 16 | }, 17 | "attr-name" => Object { 18 | "color": "rgb(255, 230, 109)", 19 | }, 20 | "parameter" => Object { 21 | "color": "rgb(255, 230, 109)", 22 | }, 23 | "script-punctuation" => Object { 24 | "color": "rgb(238, 93, 67)", 25 | }, 26 | "function" => Object { 27 | "color": "rgb(238, 93, 67)", 28 | }, 29 | "script" => Object { 30 | "color": "rgb(7, 212, 182)", 31 | }, 32 | "variable" => Object { 33 | "color": "rgb(249, 38, 114)", 34 | }, 35 | "constant" => Object { 36 | "color": "rgb(249, 38, 114)", 37 | }, 38 | "tag" => Object { 39 | "color": "rgb(249, 38, 114)", 40 | }, 41 | "char" => Object { 42 | "color": "rgb(249, 38, 114)", 43 | }, 44 | "number" => Object { 45 | "color": "rgb(243, 156, 18)", 46 | }, 47 | "builtin" => Object { 48 | "color": "rgb(255, 230, 109)", 49 | }, 50 | "at-rule" => Object { 51 | "color": "rgb(255, 230, 109)", 52 | }, 53 | "class-name" => Object { 54 | "color": "rgb(255, 230, 109)", 55 | }, 56 | "string" => Object { 57 | "color": "rgb(124, 183, 255)", 58 | }, 59 | "changed" => Object { 60 | "color": "rgb(124, 183, 255)", 61 | }, 62 | "operator" => Object { 63 | "color": "rgb(238, 93, 67)", 64 | }, 65 | "deleted" => Object { 66 | "color": "rgb(238, 93, 67)", 67 | }, 68 | "inserted" => Object { 69 | "color": "rgb(150, 224, 114)", 70 | }, 71 | } 72 | `; 73 | -------------------------------------------------------------------------------- /packages/components/src/Code/__tests__/code.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { Code, Language, PrismTheme, convertor } from ".." 4 | import { axe, toHaveNoViolations } from "jest-axe" 5 | import { cleanup, render } from "@testing-library/react" 6 | 7 | import { ThemeProvider } from '@emotion/react' 8 | import { matchers } from "@emotion/jest" 9 | import { theme } from "../.." 10 | 11 | expect.extend(matchers) 12 | expect.extend(toHaveNoViolations) 13 | 14 | describe("", () => { 15 | let code: string 16 | let codeTheme: PrismTheme 17 | 18 | beforeEach(() => { 19 | code = ` 20 | const add = (a: number, b: number): number => { 21 | return a + b; 22 | } 23 | ` 24 | codeTheme = theme.code.theme 25 | }) 26 | 27 | afterEach(() => { 28 | code = (undefined as unknown) as string 29 | codeTheme = (undefined as unknown) as PrismTheme 30 | }) 31 | 32 | it("should not have accessibility violations (pre)", async done => { 33 | const { container, unmount } = render( 34 | 35 | , 36 | , 37 | ) 38 | const pre = container.querySelector("pre") as HTMLPreElement 39 | const a11yResults = await axe(pre) 40 | expect(a11yResults).toHaveNoViolations() 41 | cleanup() 42 | unmount() 43 | done() 44 | }) 45 | 46 | describe("Code generated Theme", () => { 47 | it("should use the default Andromeda theme", () => { 48 | const { container, unmount } = render( 49 | 50 | 51 | , 52 | ) 53 | const pre = container.querySelector("pre") 54 | expect(pre).toHaveStyleRule("color", codeTheme.plain.color) 55 | expect(pre).toHaveStyleRule( 56 | "background-color", 57 | codeTheme.plain.backgroundColor, 58 | ) 59 | unmount() 60 | }) 61 | 62 | it("should provide the generated spans with the corresponding theme token styling", () => { 63 | const themeAndromeda = convertor(codeTheme) 64 | const { container, unmount } = render( 65 | 66 | , 67 | , 68 | ) 69 | const codeEl = container.querySelector("code") as HTMLElement 70 | Array.from(codeEl.querySelectorAll("span")).forEach( 71 | (span: HTMLSpanElement) => { 72 | const styles = getComputedStyle(span) 73 | if (Boolean(styles.getPropertyValue("color"))) { 74 | expect( 75 | Array.from(themeAndromeda.values()).find( 76 | val => val.color === styles.getPropertyValue("color"), 77 | ), 78 | ).toBeTruthy() 79 | } 80 | }, 81 | ) 82 | unmount() 83 | }) 84 | }) 85 | 86 | describe("Font-Size", () => { 87 | it("should use the defined S font family", () => { 88 | const { container, unmount } = render( 89 | 90 | , 91 | , 92 | ) 93 | const pre = container.querySelector("pre") 94 | expect(pre).toHaveStyleRule("font-size", `${theme.fontSizes[0]}px`) 95 | unmount() 96 | }) 97 | 98 | it("should use the defined M font family", () => { 99 | const { container, unmount } = render( 100 | 101 | , 102 | , 103 | ) 104 | const pre = container.querySelector("pre") 105 | expect(pre).toHaveStyleRule("font-size", `${theme.fontSizes[1]}px`) 106 | unmount() 107 | }) 108 | 109 | it("should use the defined L font family", () => { 110 | const { container, unmount } = render( 111 | 112 | , 113 | , 114 | ) 115 | const pre = container.querySelector("pre") 116 | expect(pre).toHaveStyleRule("font-size", `${theme.fontSizes[2]}px`) 117 | unmount() 118 | }) 119 | }) 120 | }) 121 | -------------------------------------------------------------------------------- /packages/components/src/Code/__tests__/convertor.test.ts: -------------------------------------------------------------------------------- 1 | import { convertor, PrismTheme, PrismStyleRule } from '..'; 2 | import { andromeda } from '../../theme/code-theme'; 3 | 4 | describe('convertor()', () => { 5 | let stylePropMock1: PrismStyleRule[]; 6 | let stylePropDuplicate: PrismStyleRule[]; 7 | let stylePropDuplicateII: PrismStyleRule[]; 8 | 9 | beforeEach(() => { 10 | stylePropMock1 = [ 11 | { 12 | types: ['punctuation'], 13 | style: { 14 | color: 'rgb(249, 38, 114)', 15 | }, 16 | }, 17 | { 18 | types: ['builtin', 'at-rule', 'function'], 19 | style: { 20 | color: 'rgb(213, 206, 217)', 21 | }, 22 | }, 23 | ]; 24 | stylePropDuplicate = [ 25 | { 26 | types: ['punctuation'], 27 | style: { 28 | color: 'rgb(249, 38, 114)', 29 | backgroundColor: 'rgb(255, 255, 255)', 30 | }, 31 | }, 32 | { 33 | types: ['punctuation'], 34 | style: { 35 | color: 'rgb(213, 206, 217)', 36 | backgroundColor: 'rgb(0, 0, 0)', 37 | }, 38 | }, 39 | ]; 40 | 41 | stylePropDuplicateII = [ 42 | { 43 | types: ['punctuation'], 44 | style: { 45 | color: 'rgb(213, 206, 217)', 46 | }, 47 | }, 48 | { 49 | types: ['punctuation'], 50 | style: { 51 | color: 'rgb(249, 38, 114)', 52 | backgroundColor: 'rgb(0, 0, 0)', 53 | }, 54 | }, 55 | ]; 56 | }); 57 | 58 | afterEach(() => { 59 | stylePropMock1 = (undefined as unknown) as PrismStyleRule[]; 60 | stylePropDuplicate = (undefined as unknown) as PrismStyleRule[]; 61 | stylePropDuplicateII = (undefined as unknown) as PrismStyleRule[]; 62 | }); 63 | 64 | describe('Error handling', () => { 65 | beforeEach(() => { 66 | global.console = ({ error: jest.fn() } as unknown) as Console; 67 | }); 68 | 69 | it('should log an error on missing styles prop', () => { 70 | convertor({} as PrismTheme); 71 | expect(console.error).toHaveBeenCalled(); 72 | expect(console.error).toHaveBeenCalledWith( 73 | 'Error: Your theme should have a styles property (PrismStyles[])', 74 | ); 75 | }); 76 | 77 | it('should log an error on missing types prop', () => { 78 | convertor({ 79 | styles: [ 80 | { 81 | style: { 82 | color: 'rgb(95, 97, 103)', 83 | }, 84 | }, 85 | ], 86 | } as PrismTheme); 87 | expect(console.error).toHaveBeenCalled(); 88 | expect(console.error).toHaveBeenCalledWith( 89 | 'Error: Your theme should have a types property (string[])', 90 | ); 91 | }); 92 | 93 | it('should log an error on missing types prop', () => { 94 | convertor({ 95 | styles: [ 96 | { 97 | types: ['constant'], 98 | style: { 99 | color: 'rgb(213, 206, 217)', 100 | }, 101 | }, 102 | { 103 | types: ['punctuation'], 104 | style: { 105 | color: 'rgb(249, 38, 114)', 106 | }, 107 | }, 108 | { 109 | types: ['variable', 'tag', 'char'], 110 | style: {}, 111 | }, 112 | ], 113 | } as PrismTheme); 114 | expect(console.error).toHaveBeenCalled(); 115 | expect(console.error).toHaveBeenCalledWith( 116 | 'Error: Your theme should have a style property (PrismStyleProp)', 117 | ); 118 | }); 119 | }); 120 | 121 | describe('Create theme map', () => { 122 | const { plain } = andromeda; 123 | it('should return a codeTheme with the according value', () => { 124 | const codeTheme = convertor({ plain, styles: stylePropMock1 }); 125 | 126 | expect(codeTheme.get('punctuation')).toStrictEqual({ 127 | color: 'rgb(249, 38, 114)', 128 | }); 129 | }); 130 | 131 | it.only('should return the first found properties on duplicated styles', () => { 132 | const codeTheme = convertor({ plain, styles: stylePropDuplicate }); 133 | 134 | expect(codeTheme.get('punctuation')).toStrictEqual({ 135 | color: 'rgb(249, 38, 114)', 136 | backgroundColor: 'rgb(255, 255, 255)', 137 | }); 138 | }); 139 | 140 | it('should return the first found properties on duplicated styles and the last missing properties', () => { 141 | const codeTheme = convertor({ plain, styles: stylePropDuplicateII }); 142 | expect(codeTheme.get('punctuation')).toStrictEqual({ 143 | color: 'rgb(213, 206, 217)', 144 | backgroundColor: 'rgb(0, 0, 0)', 145 | }); 146 | }); 147 | 148 | it('should match the snapshot map', () => { 149 | const codeTheme = convertor(andromeda); 150 | expect(codeTheme).toMatchSnapshot(); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /packages/components/src/Code/__tests__/normalizer.test.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'prismjs'; 2 | import { normalizer } from '..'; 3 | import { prismTokens } from '../__mocks__/tokens'; 4 | 5 | describe('normalizer()', () => { 6 | describe('Error handling', () => { 7 | beforeEach(() => { 8 | global.console = ({ error: jest.fn() } as unknown) as Console; 9 | }); 10 | 11 | it('should log an error on missing styles prop', () => { 12 | const tokens = normalizer({} as (string | Token)[]); 13 | expect(tokens).toEqual([]); 14 | expect(console.error).toHaveBeenCalled(); 15 | expect(console.error).toHaveBeenCalledWith( 16 | 'Error: The provided token stream must be of type array returned by the Prism.tokenize(code, grammar)', 17 | ); 18 | }); 19 | }); 20 | 21 | describe('Flattened Token Array', () => { 22 | let prismTokensMock: (string | Token)[]; 23 | beforeEach(() => { 24 | prismTokensMock = [ 25 | { 26 | type: 'tag', 27 | content: [ 28 | { 29 | type: 'tag', 30 | content: [ 31 | { 32 | type: 'punctuation', 33 | content: '<', 34 | }, 35 | 'pre', 36 | ], 37 | }, 38 | ' ', 39 | { 40 | type: 'attr-name', 41 | content: ['sx'], 42 | }, 43 | { 44 | type: 'script', 45 | content: [ 46 | { 47 | type: 'script-punctuation', 48 | content: '=', 49 | }, 50 | { 51 | type: 'punctuation', 52 | content: '{', 53 | }, 54 | { 55 | type: 'function', 56 | content: 'createStylesPre', 57 | }, 58 | 'props', 59 | ], 60 | alias: 'language-javascript', 61 | }, 62 | ], 63 | }, 64 | '\n ', 65 | ] as (string | Token)[]; 66 | }); 67 | 68 | afterEach(() => { 69 | prismTokensMock = (undefined as unknown) as (string | Token)[]; 70 | }); 71 | 72 | it('should return a flattened token array from the prismTokensMock Mock', () => { 73 | const result = [ 74 | { type: 'punctuation', content: '<' }, 75 | { type: 'tag', content: 'pre', alias: '', length: 0, greedy: false }, 76 | { type: 'tag', content: ' ', alias: '', length: 0, greedy: false }, 77 | { type: 'attr-name', content: 'sx', alias: '', length: 0, greedy: false }, 78 | { type: 'script-punctuation', content: '=' }, 79 | { type: 'punctuation', content: '{' }, 80 | { type: 'function', content: 'createStylesPre' }, 81 | { type: 'script', content: 'props', alias: '', length: 0, greedy: false }, 82 | { type: 'tag', content: '\n ', alias: '', length: 0, greedy: false }, 83 | ]; 84 | expect(normalizer(prismTokensMock)).toEqual(result); 85 | }); 86 | it('should return a flattened token array from a returned by Prism.tokenize() (___mocks___)', () => { 87 | expect(normalizer(prismTokens)).toMatchSnapshot(); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /packages/components/src/Code/convertor.ts: -------------------------------------------------------------------------------- 1 | import { PrismStyleProp, PrismStyleRule, PrismTheme } from '.'; 2 | 3 | export type CodeTheme = Map; 4 | 5 | export function isEmptyObj(obj: T): boolean { 6 | for (const prop in obj) { 7 | if ((obj as Record).hasOwnProperty(prop)) { 8 | return false; 9 | } 10 | } 11 | return true; 12 | } 13 | 14 | export function convertor(prism: PrismTheme): CodeTheme { 15 | const theme = new Map(); 16 | try { 17 | if (!prism.styles || prism.styles.length < 1) { 18 | throw Error('Your theme should have a styles property (PrismStyles[])'); 19 | } 20 | 21 | prism.styles.forEach((styleRule: PrismStyleRule): void => { 22 | const { types, style } = styleRule; 23 | 24 | if (!types || types.length === 0) { 25 | throw Error('Your theme should have a types property (string[])'); 26 | } 27 | if (isEmptyObj(style)) { 28 | throw Error('Your theme should have a style property (PrismStyleProp)'); 29 | } 30 | 31 | types.forEach(type => { 32 | theme.has(type) 33 | ? theme.set(type, { ...style, ...theme.get(type) }) 34 | : theme.set(type, { ...style }); 35 | }); 36 | }); 37 | } catch (err) { 38 | console.error(`${err}`); 39 | } 40 | return theme; 41 | } 42 | -------------------------------------------------------------------------------- /packages/components/src/Code/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Code'; 2 | export * from './convertor'; 3 | export * from './normalizer'; 4 | -------------------------------------------------------------------------------- /packages/components/src/Code/normalizer.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'prismjs'; 2 | 3 | export function normalizer(stream: (string | Token)[], type = 'plain'): Token[] { 4 | try { 5 | if (!Array.isArray(stream)) { 6 | throw Error('The provided token stream must be of type array returned by the Prism.tokenize(code, grammar)'); 7 | } 8 | return stream.reduce( 9 | (acc: Token[], curr: string | Token) => 10 | acc.concat( 11 | typeof curr === 'object' && Array.isArray(curr.content) 12 | ? normalizer(curr.content, (type = curr.type)) 13 | : createToken(curr, type), 14 | ), 15 | [], 16 | ); 17 | } catch (err) { 18 | console.error(`${err}`); 19 | return []; 20 | } 21 | } 22 | 23 | export const createToken = (token: Token | string, type: string): Token => ({ 24 | ...(typeof token !== 'object' ? { type, content: token, alias: '', length: 0, greedy: false } : token), 25 | }); 26 | -------------------------------------------------------------------------------- /packages/components/src/Column.tsx: -------------------------------------------------------------------------------- 1 | import { Scale, ScaleArea } from "." 2 | 3 | import React from "react" 4 | import { css } from "@emotion/react" 5 | import styled from "@emotion/styled" 6 | 7 | export interface ColumnProps { 8 | readonly as?: HTMLRowType 9 | readonly basis?: RowFlexBasis 10 | readonly order?: number 11 | readonly padding?: ScaleArea 12 | } 13 | 14 | export type RowFlexBasis = 15 | | "fluid" 16 | | "1/2" 17 | | "1/3" 18 | | "2/3" 19 | | "1/4" 20 | | "3/4" 21 | | "1/5" 22 | | "2/5" 23 | | "3/5" 24 | | "4/5" 25 | 26 | export type HTMLRowType = "div" | "section" | "aside" | "article" 27 | 28 | const calculateFlexBasis = (basis: RowFlexBasis): string => 29 | basis 30 | .split("/") 31 | .reduce((prev: string, curr: string) => ((+prev * 100) / +curr).toString()) 32 | 33 | const StyledColumn = styled.div` 34 | box-sizing: border-box; 35 | ${props => { 36 | const { basis } = props 37 | return !!basis && basis !== "fluid" 38 | ? css` 39 | flex: 0 0 ${calculateFlexBasis(basis)}%; 40 | ` 41 | : "" 42 | }}; 43 | padding: ${props => 44 | !!props.padding && 45 | Scale.toCSSPixel(Scale.create(props.padding, props.theme.space))}; 46 | order: ${props => props.order ?? "initial"}; 47 | ` 48 | 49 | StyledColumn.displayName = "StyledColumn" 50 | 51 | export const Column = (props: React.PropsWithChildren): JSX.Element => ( 52 | 57 | {props.children} 58 | 59 | ) 60 | -------------------------------------------------------------------------------- /packages/components/src/Columns.tsx: -------------------------------------------------------------------------------- 1 | import { Scale, ScaleArea } from "." 2 | import { css } from "@emotion/react" 3 | 4 | import React from "react" 5 | import styled from "@emotion/styled" 6 | 7 | export interface ColumnsProps { 8 | readonly as?: HTMLRowsType 9 | readonly align?: CSSAlign 10 | readonly collapseBelow?: number 11 | readonly justifyContent?: CSSJustify 12 | readonly padding?: ScaleArea 13 | } 14 | 15 | export type HTMLRowsType = 16 | | "div" 17 | | "section" 18 | | "main" 19 | | "article" 20 | | "nav" 21 | | "footer" 22 | | "header" 23 | 24 | export type CSSJustify = 25 | | "center" 26 | | "end" 27 | | "start" 28 | | "flex-end" 29 | | "flex-start" 30 | | "stretch" 31 | | "space-around" 32 | | "space-between" 33 | | "space-evenly" 34 | 35 | export type CSSAlign = 36 | | "center" 37 | | "baseline" 38 | | "flex-end" 39 | | "flex-start" 40 | | "self-end" 41 | | "self-start" 42 | 43 | const StyledColumns = styled.div` 44 | display: flex; 45 | flex-direction: row; 46 | ${props => { 47 | const { justifyContent } = props 48 | return !!justifyContent ? `justify-content: ${justifyContent};` : "" 49 | }} 50 | 51 | ${props => { 52 | const { align } = props 53 | return !!align ? `align-items: ${align};` : "" 54 | }} 55 | padding: ${props => 56 | !!props.padding && 57 | Scale.toCSSPixel(Scale.create(props.padding, props.theme.space))}; 58 | 59 | ${props => { 60 | const { collapseBelow, theme } = props 61 | return !!collapseBelow 62 | ? css` 63 | @media screen and (max-width: ${theme.breakpoints[collapseBelow]}) { 64 | flex-direction: column; 65 | } 66 | ` 67 | : "" 68 | }}; 69 | ` 70 | 71 | StyledColumns.displayName = "StyledColumns" 72 | 73 | export const Columns = (props: React.PropsWithChildren): JSX.Element => ( 74 | 80 | {props.children} 81 | 82 | ) 83 | -------------------------------------------------------------------------------- /packages/components/src/Heading.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { SerializedStyles, css, jsx } from "@emotion/react" 3 | 4 | import React from "react" 5 | import { Theme } from "@emotion/react" 6 | import styled from "@emotion/styled" 7 | 8 | export interface HeadingProps { 9 | readonly size: HeadlineSize 10 | readonly type: HeadingFamily 11 | readonly breakpoint?: number 12 | 13 | readonly as?: HeadingLevel 14 | readonly isInverted?: boolean 15 | readonly weight?: number 16 | } 17 | 18 | export type HeadingFamily = "primary" | "secondary" | "campaign" 19 | export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "strong" 20 | export type HeadlineSize = "xs" | "s" | "m" | "l" | "xl" | "xxl" 21 | 22 | const createBreakpointStyles = ( 23 | breakpointIdx: number, 24 | size: HeadlineSize, 25 | theme: Theme, 26 | ): SerializedStyles => { 27 | return theme.heading[size].growSize 28 | ? css` 29 | @media (min-width: ${theme.breakpoints[breakpointIdx]}) { 30 | line-height: 1.5; 31 | font-size: ${theme.heading[size].growSize}px; 32 | } 33 | ` 34 | : css`` 35 | } 36 | 37 | const StyledHeading = styled.h1` 38 | margin: 0; 39 | font-kerning: normal; 40 | font-family: ${props => { 41 | const { type, theme } = props 42 | return !!theme.heading.fonts[type] ? theme.heading.fonts[type] : "" 43 | }}; 44 | 45 | font-size: ${props => { 46 | const { size, theme } = props 47 | return !!theme.heading[size] ? `${theme.heading[size].fontSize}px` : "" 48 | }}; 49 | 50 | font-weight: ${props => 51 | props.weight ? props.weight : props.theme.heading.fontWeight}; 52 | color: ${props => { 53 | const { isInverted, theme } = props 54 | return isInverted ? theme.heading.modes.color : theme.heading.color 55 | }}; 56 | 57 | line-height: 1.3; 58 | -webkit-font-smoothing: antialiased; 59 | 60 | ${({ breakpoint, size, theme }) => 61 | breakpoint ? createBreakpointStyles(breakpoint, size, theme) : ""} 62 | ` 63 | 64 | StyledHeading.displayName = "StyledHeading" 65 | 66 | export const Heading = (props: React.PropsWithChildren): JSX.Element => ( 67 | 74 | {props.children} 75 | 76 | ) 77 | -------------------------------------------------------------------------------- /packages/components/src/Layer.tsx: -------------------------------------------------------------------------------- 1 | import { Scale, ScaleArea } from "." 2 | 3 | import React from "react" 4 | import styled from "@emotion/styled" 5 | 6 | export interface LayerProps { 7 | readonly isOpen: boolean 8 | readonly padding: ScaleArea 9 | } 10 | 11 | const StyledLayer = styled.section` 12 | box-sizing: border-box; 13 | position: fixed; 14 | top: 0; 15 | left: 0; 16 | right: 0; 17 | z-index: 3; 18 | width: 100vw; 19 | height: 100vh; 20 | 21 | padding: ${props => 22 | Scale.toCSSPixel(Scale.create(props.padding, props.theme.space))}; 23 | margin: auto; 24 | opacity: ${props => (props.isOpen ? 1 : 0)}; 25 | overflow-y: scroll; 26 | overflow-x: hidden; 27 | background-color: ${props => props.theme.colors.background}; 28 | transform: ${props => 29 | props.isOpen ? "translate3d(0, 0, 0)" : "translate3d(0, 100%, 0)"}; 30 | transition: ${props => 31 | `transform ${props.theme.transition.duration[0]}s ${props.theme.transition.timing[0]}, opacity ${props.theme.transition.duration[0]}s linear`}; 32 | 33 | @media (min-width: ${props => props.theme.breakpoints[2]}) { 34 | top: ${props => props.theme.layer.top}px; 35 | width: 80vw; 36 | height: calc(100vh - ${props => props.theme.layer.top}px); 37 | max-width: ${props => props.theme.layer.maxWidth}; 38 | } 39 | ` 40 | StyledLayer.displayName = "StyledLayer" 41 | 42 | export const Layer = (props: React.PropsWithChildren): JSX.Element => ( 43 | 44 | {props.children} 45 | 46 | ) 47 | -------------------------------------------------------------------------------- /packages/components/src/LayerAside.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | 3 | import { Scale, ScaleArea } from "." 4 | import { SerializedStyles, css, jsx } from "@emotion/react" 5 | 6 | import React from "react" 7 | import styled from "@emotion/styled" 8 | 9 | export interface LayerAsideProps { 10 | readonly isOpen: boolean 11 | readonly padding: ScaleArea 12 | readonly position?: "left" | "right" 13 | } 14 | 15 | const getLayerPosition = ( 16 | position: "left" | "right" = "right", 17 | ): SerializedStyles => css` 18 | ${position}: 0; 19 | ` 20 | 21 | const getX = ( 22 | isOpen: boolean, 23 | position: "left" | "right" = "right", 24 | ): string => { 25 | if (isOpen) { 26 | return "0" 27 | } 28 | return position === "right" ? "100%" : "-100%" 29 | } 30 | 31 | const StyledLayerAside = styled.aside` 32 | ${props => getLayerPosition(props.position)}; 33 | position: fixed; 34 | top: ${props => props.theme.layerAside.top}px; 35 | z-index: 3; 36 | box-sizing: border-box; 37 | width: 100vw; 38 | height: calc(100vh - ${props => props.theme.layerAside.top}px); 39 | padding: ${props => 40 | Scale.toCSSPixel(Scale.create(props.padding, props.theme.space))}; 41 | opacity: ${props => (props.isOpen ? 1 : 0)}; 42 | overflow-y: scroll; 43 | overflow-x: hidden; 44 | background-color: ${props => props.theme.colors.background}; 45 | transform: translate3d(${props => getX(props.isOpen, props.position)}, 0, 0); 46 | transition: ${props => 47 | `transform ${props.theme.transition.duration[0]}s ${props.theme.transition.timing[0]}, opacity ${props.theme.transition.duration[0]}s linear`}; 48 | 49 | @media (min-width: ${props => props.theme.layerAside.breakpoint}) { 50 | top: 0; 51 | width: 50vw; 52 | height: 100vh; 53 | max-width: ${props => props.theme.layerAside.maxWidth}; 54 | } 55 | ` 56 | 57 | StyledLayerAside.displayName = "StyledLayerAside" 58 | 59 | export const LayerAside = (props: React.PropsWithChildren): JSX.Element => ( 60 | 64 | {props.children} 65 | 66 | ) 67 | -------------------------------------------------------------------------------- /packages/components/src/LayerShim.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import styled from "@emotion/styled" 3 | 4 | export interface ShimProps { 5 | readonly isOpen: boolean 6 | readonly onClick?: React.MouseEventHandler 7 | } 8 | 9 | const StyledLayerShim = styled.div` 10 | box-sizing: border-box; 11 | position: fixed; 12 | top: 0; 13 | right: 0; 14 | left: 0; 15 | bottom: 0; 16 | z-index: 2; 17 | margin: auto; 18 | opacity: ${props => (props.isOpen ? 0.8 : 0)}; 19 | background-color: ${props => props.theme.layer.shimColor}; 20 | pointer-events: ${props => (props.isOpen ? "auto" : "none")}; 21 | cursor: ${props => !!props.onClick && "pointer"}; 22 | transition: ${props => 23 | `opacity ${props.theme.transition.duration[0]}s linear `}; 24 | ` 25 | 26 | StyledLayerShim.displayName = "StyledLayerShim" 27 | 28 | export const LayerShim: React.FC = props => ( 29 | 30 | ) 31 | -------------------------------------------------------------------------------- /packages/components/src/Legend.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import styled from "@emotion/styled" 3 | 4 | export interface LegendProps { 5 | readonly size: LegendSize 6 | readonly as?: LegendTypes 7 | readonly type?: "primary" | "secondary" 8 | } 9 | 10 | type LegendTypes = "figcaption" | "legend" | "strong" | "small" | "span" | "div" 11 | 12 | type LegendSize = "xs" | "s" | "m" | "l" 13 | 14 | const StyledLegend = styled.div` 15 | color: ${({ theme }) => theme.legend.color}; 16 | font-family: ${({ theme }) => theme.legend.fontFamily}; 17 | font-size: ${({ size, theme }) => `${theme.legend[size].fontSize}px`}; 18 | font-kerning: normal; 19 | font-weight: normal; 20 | line-height: ${props => props.theme.lineHeights[1]}; 21 | letter-spacing: 2px; 22 | text-transform: ${({ type }) => 23 | type === "primary" ? "uppercase" : "capitalize"}; 24 | -webkit-font-smoothing: antialiased; 25 | ` 26 | 27 | StyledLegend.displayName = "StyledLegend" 28 | 29 | export const Legend = (props: React.PropsWithChildren): JSX.Element => ( 30 | 34 | {props.children} 35 | 36 | ) 37 | -------------------------------------------------------------------------------- /packages/components/src/Link.tsx: -------------------------------------------------------------------------------- 1 | import { SerializedStyles, css } from "@emotion/react" 2 | 3 | import React from "react" 4 | import { Theme } from "." 5 | import styled from "@emotion/styled" 6 | 7 | export interface LinkProps { 8 | readonly size: "xs" | "s" | "m" | "l" | "xl" 9 | readonly as?: "a" | "span" | "button" 10 | readonly color?: "primary" | "secondary" | "tertiary" 11 | readonly href?: string 12 | readonly isActive?: boolean 13 | readonly onClick?: React.MouseEventHandler 14 | readonly type?: "inline" | "block" 15 | } 16 | 17 | interface StyledLinkProps { 18 | readonly size: "xs" | "s" | "m" | "l" | "xl" 19 | readonly as?: "a" | "span" | "button" 20 | readonly isActive?: boolean 21 | readonly color?: "primary" | "secondary" | "tertiary" 22 | readonly onClick?: React.MouseEventHandler 23 | readonly type?: "inline" | "block" 24 | } 25 | 26 | const stylesUnderline = ( 27 | props: { 28 | readonly theme: Theme 29 | } & StyledLinkProps, 30 | ): SerializedStyles => css` 31 | ::after { 32 | content: ""; 33 | position: absolute; 34 | bottom: 0; 35 | right: 100%; 36 | width: 100%; 37 | height: 1px; 38 | background-color: currentColor; 39 | transform: ${props.type === "inline" || props.isActive 40 | ? "translate3d(100%, 0, 0)" 41 | : "none"}; 42 | transition: transform ${props.theme.transition.duration[0]}s 43 | ${props.theme.transition.timing[0]}; 44 | } 45 | ${!props.isActive && 46 | `:hover { 47 | ::after { 48 | transform: ${ 49 | props.type === "inline" ? "none" : "translate3d(100%, 0, 0)" 50 | }; 51 | } 52 | }`} 53 | ` 54 | 55 | const StyledLink = styled.a` 56 | position: relative; 57 | display: inline-block; 58 | padding: 0; 59 | margin: 0; 60 | border: none; 61 | overflow: hidden; 62 | font-family: ${props => props.theme.link.fontFamily}; 63 | font-size: ${props => { 64 | const { size, theme } = props 65 | return !!theme.link[size] ? `${theme.link[size].fontSize}px` : "" 66 | }}; 67 | color: ${props => { 68 | const { color, theme } = props 69 | return !!color 70 | ? theme.link.color[color].default 71 | : theme.link.color.primary.default 72 | }}; 73 | font-kerning: normal; 74 | text-decoration: none; 75 | line-height: ${props => props.theme.lineHeights[1]}; 76 | letter-spacing: 1px; 77 | cursor: pointer; 78 | -webkit-font-smoothing: antialiased; 79 | ${props => stylesUnderline(props)}; 80 | 81 | :hover { 82 | color: ${props => { 83 | const { color, theme } = props 84 | return !!color 85 | ? theme.link.color[color].hover 86 | : theme.link.color.primary.hover 87 | }}; 88 | } 89 | ` 90 | StyledLink.displayName = "StyledLink" 91 | 92 | export const Link = (props: React.PropsWithChildren): JSX.Element => ( 93 | 101 | {props.children} 102 | 103 | ) 104 | -------------------------------------------------------------------------------- /packages/components/src/Select/Frame.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { calcSize } from "./utils" 3 | import styled from "@emotion/styled" 4 | 5 | export interface SelectFrameProps { 6 | readonly ariaLabel: string 7 | readonly ariaActivedescendant: string 8 | readonly fontSize: number 9 | } 10 | 11 | interface StyledFrameProps { 12 | readonly fontSize: number 13 | } 14 | 15 | const StyledFrame = styled.ul` 16 | display: grid; 17 | grid-template-columns: ${props => 18 | `repeat(auto-fit, ${calcSize(props.fontSize, props.theme.fontSizes)}px)`}; 19 | grid-column-gap: ${props => 20 | `${calcSize(props.fontSize, props.theme.fontSizes) / 2.5}px`}; 21 | padding: 0; 22 | margin: 0; 23 | outline: none; 24 | ` 25 | 26 | StyledFrame.displayName = "Select.StyledFrame" 27 | 28 | export const Frame = (props: React.PropsWithChildren): JSX.Element => ( 29 | 35 | {props.children} 36 | 37 | ) 38 | 39 | Frame.displayName = "Select.Frame" 40 | -------------------------------------------------------------------------------- /packages/components/src/Select/Item.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { SerializedStyles, css, jsx } from "@emotion/react" 3 | 4 | import { Theme } from ".." 5 | import { calcSize } from "./utils" 6 | import styled from "@emotion/styled" 7 | import React from "react" 8 | 9 | export interface SelectTileProps { 10 | readonly id: string 11 | readonly fontSize: number 12 | readonly isActive: boolean 13 | readonly isAvailable: boolean 14 | readonly onClick: React.MouseEventHandler 15 | readonly borderWidth?: number 16 | } 17 | 18 | interface StyledSelectItemProps { 19 | readonly borderWidth?: number 20 | readonly isActive: boolean 21 | readonly isAvailable: boolean 22 | readonly fontSize: number 23 | } 24 | 25 | const createStylesSize = ( 26 | theme: Theme, 27 | borderWidth: number, 28 | fontSize: number, 29 | ): SerializedStyles => css` 30 | width: ${calcSize(fontSize, theme.fontSizes)}px; 31 | height: ${calcSize(fontSize, theme.fontSizes)}px; 32 | border-width: ${borderWidth < theme.borderWidths.length 33 | ? `${theme.borderWidths[borderWidth]}px` 34 | : `${theme.borderWidths[0]}px`}; 35 | border-style: solid; 36 | line-height: ${calcSize(fontSize, theme.fontSizes)}px; 37 | ` 38 | 39 | const createActiveStyles = ( 40 | theme: Theme, 41 | isActive: boolean, 42 | isAvailable: boolean, 43 | ): SerializedStyles => css` 44 | color: ${isAvailable ? "currentColor" : theme.colors.borderDisabled}; 45 | border-color: ${isAvailable 46 | ? isActive 47 | ? theme.colors.borderActive 48 | : theme.colors.border 49 | : theme.colors.borderDisabled}; 50 | cursor: ${isAvailable ? "pointer" : "unset"}; 51 | pointer-events: ${isAvailable ? "all" : "none"}; 52 | text-decoration: ${isAvailable ? "none" : "line-through"}; 53 | ` 54 | 55 | const StyledItem = styled.li` 56 | ${({ borderWidth = 1, fontSize, theme }) => 57 | createStylesSize(theme, borderWidth, fontSize)}; 58 | ${({ theme, isActive, isAvailable }) => 59 | createActiveStyles(theme, isActive, isAvailable)} 60 | 61 | list-style: none; 62 | font-family: ${({ theme }) => theme.fonts.body}; 63 | font-size: ${({ fontSize, theme }) => 64 | fontSize < theme.fontSizes.length 65 | ? `${theme.fontSizes[fontSize]}px` 66 | : `${theme.fontSizes[0]}px`}; 67 | text-align: center; 68 | ` 69 | 70 | StyledItem.displayName = "Select.StyledItem" 71 | 72 | export const Item = (props: React.PropsWithChildren): JSX.Element => ( 73 | 82 | {props.children} 83 | 84 | ) 85 | 86 | Item.displayName = "Select.Item" 87 | -------------------------------------------------------------------------------- /packages/components/src/Select/index.ts: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import * as Frame from "./Frame" 3 | import * as Item from "./Item" 4 | 5 | interface Props { 6 | Item: { 7 | (props: React.PropsWithChildren): JSX.Element 8 | } 9 | Frame: { 10 | (props: React.PropsWithChildren): JSX.Element 11 | displayName: string 12 | } 13 | } 14 | 15 | export const Select: Props = { ...Frame, ...Item } 16 | -------------------------------------------------------------------------------- /packages/components/src/Select/utils.ts: -------------------------------------------------------------------------------- 1 | export const calcSize = (fontSize: number, fontSizes: number[]): number => 2 | fontSize < fontSizes.length ? fontSizes[fontSize] * 2.5 : fontSizes[0] * 2.5; 3 | -------------------------------------------------------------------------------- /packages/components/src/Small.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import styled from "@emotion/styled" 3 | 4 | export interface SmallProps { 5 | readonly size: "s" | "m" | "l" 6 | readonly color?: 7 | | "primary" 8 | | "secondary" 9 | | "text" 10 | | "muted" 11 | | "background" 12 | | "mutedHover" 13 | } 14 | 15 | const StyledSmall = styled.small` 16 | color: ${props => { 17 | const { color, theme } = props 18 | return !!color && theme.colors[color] 19 | ? theme.colors[color] 20 | : theme.colors.primary 21 | }}; 22 | font-family: ${props => props.theme.small.fontFamily}; 23 | font-size: ${props => { 24 | const { size, theme } = props 25 | return !!theme.small[size] ? `${theme.small[size].fontSize}px` : "" 26 | }}; 27 | font-kerning: normal; 28 | letter-spacing: 1px; 29 | -webkit-font-smoothing: antialiased; 30 | line-height: 1.5; 31 | ` 32 | StyledSmall.displayName = "StyledSmall" 33 | 34 | export const Small = (props: React.PropsWithChildren): JSX.Element => ( 35 | 36 | {props.children} 37 | 38 | ) 39 | -------------------------------------------------------------------------------- /packages/components/src/Stack.tsx: -------------------------------------------------------------------------------- 1 | import { BoxHTMLElement } from "." 2 | import React from "react" 3 | import styled from "@emotion/styled" 4 | 5 | export interface StackProps { 6 | readonly as: BoxHTMLElement 7 | readonly space: number 8 | } 9 | 10 | const StyledStack = styled.div` 11 | display: grid; 12 | grid-row-gap: ${({ space, theme }) => 13 | `${ 14 | theme.space[space] && theme.space[space] > 0 15 | ? theme.space[space] 16 | : theme.space[1] 17 | }px`}; 18 | padding: 0; 19 | margin: 0; 20 | ` 21 | StyledStack.displayName = "StyledStack" 22 | 23 | export const Stack = (props: React.PropsWithChildren): JSX.Element => ( 24 | 25 | {props.children} 26 | 27 | ) 28 | -------------------------------------------------------------------------------- /packages/components/src/Table/Cell.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Scale } from ".." 3 | import { css } from "@emotion/react" 4 | import styled from "@emotion/styled" 5 | 6 | export interface TableCellProps { 7 | readonly as?: "td" | "th" 8 | readonly borderless?: boolean 9 | readonly firstWidth?: number 10 | } 11 | 12 | const StyledTableCell = styled.td` 13 | padding: ${props => 14 | Scale.toCSSPixel( 15 | Scale.create(props.theme.table.cellPadding, props.theme.space), 16 | )}; 17 | border-color: ${props => props.theme.table.color}; 18 | border-style: solid; 19 | border-width: ${props => (props.borderless ? "0" : "0 0 1px 0")}; 20 | text-align: center; 21 | color: ${props => 22 | props.as === "td" ? props.theme.table.color : props.theme.colors.text}; 23 | font-family: ${props => props.theme.table.fontFamily}; 24 | font-size: ${props => props.theme.table.fontSize}px; 25 | font-weight: normal; 26 | -webkit-font-smoothing: antialiased; 27 | 28 | :first-of-type { 29 | ${props => { 30 | if (!props.firstWidth) return "" 31 | return css` 32 | width: ${props.firstWidth}px; 33 | ` 34 | }} 35 | position: sticky; 36 | border-width: ${props => (props.borderless ? "0 1px 0 0" : "0 1px 1px 0")}; 37 | text-align: left; 38 | } 39 | ` 40 | StyledTableCell.displayName = "StyledTableCell" 41 | 42 | export const Cell = (props: React.PropsWithChildren): JSX.Element => ( 43 | 47 | {props.children} 48 | 49 | ) 50 | 51 | Cell.displayName = "Table.Cell" 52 | -------------------------------------------------------------------------------- /packages/components/src/Table/Frame.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import styled from "@emotion/styled" 3 | 4 | const StyledTableFrame = styled.table` 5 | width: 100%; 6 | border-collapse: collapse; 7 | ` 8 | StyledTableFrame.displayName = "StyledTableFrame" 9 | 10 | export const Frame = (props: React.PropsWithChildren>): JSX.Element => ( 11 | {props.children} 12 | ) 13 | 14 | Frame.displayName = "Table.Frame" 15 | -------------------------------------------------------------------------------- /packages/components/src/Table/Table.test.tsx: -------------------------------------------------------------------------------- 1 | import { Table, Theme, theme as rawTheme } from ".." 2 | import { cleanup, render } from "../../testing-library" 3 | 4 | import React from "react" 5 | import { axe } from "jest-axe" 6 | import { matchers } from "@emotion/jest" 7 | 8 | expect.extend(matchers) 9 | 10 | describe("", () => { 11 | let id: string 12 | 13 | beforeEach(() => { 14 | id = "table-id" 15 | }) 16 | 17 | afterEach(() => { 18 | id = (undefined as unknown) as string 19 | }) 20 | 21 | const theme = rawTheme as Theme 22 | 23 | describe("Accessibility", () => { 24 | it("should not have any accessibility violations", async done => { 25 | const { container, unmount } = render( 26 | 27 | 28 | 29 | Woodlike  30 | XS 31 | 32 | 33 | 34 | 35 | DE 36 | 34/36 37 | 38 | 39 | 40 | JPN 41 | 9/11 42 | 43 | 44 | , 45 | ) 46 | 47 | const a11yResults = await axe(container) 48 | expect(a11yResults).toHaveNoViolations() 49 | cleanup() 50 | unmount() 51 | done() 52 | }) 53 | }) 54 | 55 | describe("Table Cell", () => { 56 | it("should have the default themed border styled", () => { 57 | const { getByText, unmount } = render( 58 | 59 | 60 | 61 | {id} 62 | 63 | 64 | , 65 | ) 66 | 67 | const cell = getByText(id) 68 | expect(cell).toHaveStyleRule("border-width", "0 0 1px 0") 69 | expect(cell).toHaveStyleRule("border-width", "0 1px 1px 0", { 70 | target: ":first-of-type", 71 | }) 72 | unmount() 73 | }) 74 | 75 | it("should have the borderless themed border styled", () => { 76 | const { getByText, unmount } = render( 77 | 78 | 79 | 80 | {id} 81 | 82 | 83 | , 84 | ) 85 | 86 | const cell = getByText(id) 87 | expect(cell).toHaveStyleRule("border-width", "0") 88 | expect(cell).toHaveStyleRule("border-width", "0 1px 0 0", { 89 | target: ":first-of-type", 90 | }) 91 | unmount() 92 | }) 93 | 94 | it("should have a default theme cell color for a table cell", () => { 95 | const { getByText, unmount } = render( 96 | 97 | 98 | 99 | {id} 100 | 101 | 102 | , 103 | ) 104 | const cell = getByText(id) 105 | expect(cell).toHaveStyleRule("color", theme.table.color) 106 | unmount() 107 | }) 108 | 109 | it("should have a default theme text color for the table header cell color", () => { 110 | const { getByText, unmount } = render( 111 | 112 | 113 | 114 | 115 | {id} 116 | 117 | 118 | 119 | , 120 | ) 121 | const cell = getByText(id) 122 | expect(cell).toHaveStyleRule("color", theme.colors.text) 123 | unmount() 124 | }) 125 | 126 | it("should have a defined theme cell color", () => { 127 | const { getByText, unmount } = render( 128 | 129 | 130 | 131 | 132 | {id} 133 | 134 | 135 | 136 | , 137 | ) 138 | const cell = getByText(id) 139 | expect(cell).toHaveStyleRule("color", theme.table.color) 140 | unmount() 141 | }) 142 | 143 | it("should have a fixed width", () => { 144 | const { getByText, unmount } = render( 145 | 146 | 147 | 148 | 149 | {id} 150 | 151 | {""} 152 | 153 | 154 | , 155 | ) 156 | const cell = getByText(id) 157 | expect(cell).toHaveStyleRule("width", `${theme.space[2]}px`, { 158 | target: ":first-of-type", 159 | }) 160 | unmount() 161 | }) 162 | }) 163 | }) 164 | -------------------------------------------------------------------------------- /packages/components/src/Table/index.ts: -------------------------------------------------------------------------------- 1 | import * as Cell from './Cell'; 2 | import * as Frame from './Frame'; 3 | 4 | export const Table = { ...Frame, ...Cell }; 5 | -------------------------------------------------------------------------------- /packages/components/src/Text.tsx: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import { SerializedStyles, css, jsx } from "@emotion/react" 3 | 4 | import { Theme } from "." 5 | import styled from "@emotion/styled" 6 | 7 | export interface TextProps { 8 | readonly size: TextSize 9 | readonly breakpoint?: number 10 | readonly as?: TextType 11 | readonly isInverted?: boolean 12 | readonly weight?: 300 | 500 | 700 13 | } 14 | 15 | type TextSize = "s" | "m" | "l" 16 | export type TextType = "p" | "div" | "span" 17 | 18 | const createBreakpointStyles = ( 19 | breakpointIdx: number, 20 | size: TextSize, 21 | theme: Theme, 22 | ): SerializedStyles => { 23 | return theme.text[size].growSize 24 | ? css` 25 | @media (min-width: ${theme.breakpoints[breakpointIdx]}) { 26 | line-height: 1.4; 27 | font-size: ${theme.text[size].growSize}px; 28 | } 29 | ` 30 | : css`` 31 | } 32 | 33 | const StyledText = styled.p` 34 | margin: 0; 35 | font-family: ${({ theme }) => theme.text.fontFamily}; 36 | font-kerning: normal; 37 | font-size: ${({ size, theme }) => `${theme.text[size].fontSize}px`}; 38 | line-height: ${props => props.theme.lineHeights[1]}; 39 | letter-spacing: 0.2px; 40 | color: ${({ isInverted, theme }) => 41 | !!isInverted ? theme.text.modes.color : theme.text.color}; 42 | font-weight: ${({ weight }) => weight ?? 300}; 43 | -webkit-font-smoothing: antialiased; 44 | 45 | ${({ breakpoint, size, theme }) => 46 | breakpoint ? createBreakpointStyles(breakpoint, size, theme) : ""} 47 | ` 48 | 49 | StyledText.displayName = "StyledText" 50 | 51 | export const Text = (props: React.PropsWithChildren): JSX.Element => ( 52 | 58 | {props.children} 59 | 60 | ) 61 | -------------------------------------------------------------------------------- /packages/components/src/Video/Controls.tsx: -------------------------------------------------------------------------------- 1 | import { keyframes } from "@emotion/react" 2 | 3 | import styled from "@emotion/styled" 4 | import React from "react" 5 | 6 | export interface ControlProps { 7 | readonly size: number 8 | readonly muted: boolean 9 | readonly isPlaying: boolean 10 | readonly mutedIcon?: JSX.Element 11 | readonly loudIcon?: JSX.Element 12 | readonly playIcon?: JSX.Element 13 | readonly pauseIcon?: JSX.Element 14 | readonly onClick: React.MouseEventHandler 15 | } 16 | 17 | const heartbeat = keyframes` 18 | 0% { 19 | transform: scale(1); 20 | } 21 | 20% { 22 | transform: scale(1.25); 23 | } 24 | 40% { 25 | transform: scale(1); 26 | } 27 | 60% { 28 | transform: scale(1.25); 29 | } 30 | 80% { 31 | transform: scale(1); 32 | } 33 | 100% { 34 | transform: scale(1); 35 | } 36 | ` 37 | 38 | const StyledControlMuted = styled.div` 39 | display: inline-block; 40 | cursor: pointer; 41 | animation: ${heartbeat} 1.8s infinite cubic-bezier(0.455, 0.03, 0.515, 0.955); 42 | transition: color ${props => props.theme.transition.duration[0]}s linear; 43 | :hover { 44 | color: ${props => props.theme.video.controls.color}; 45 | animation-play-state: paused; 46 | } 47 | ` 48 | 49 | StyledControlMuted.displayName = "Video.StyledControlMuted" 50 | 51 | const StyledControlLoud = styled.div` 52 | display: inline-block; 53 | cursor: pointer; 54 | transition: color ${props => props.theme.transition.duration[0]}s linear; 55 | :hover { 56 | color: ${props => props.theme.video.controls.color}; 57 | } 58 | ` 59 | 60 | StyledControlLoud.displayName = "Video.StyledControlLoud" 61 | 62 | const StyledControlItem = styled.div` 63 | display: inline-block; 64 | cursor: pointer; 65 | padding-right: ${props => props.theme.space[2]}px; 66 | ` 67 | 68 | StyledControlItem.displayName = "StyledControlPlay" 69 | 70 | export const Controls = ( 71 | props: React.PropsWithChildren, 72 | ): JSX.Element => { 73 | return ( 74 | <> 75 | {!!props.playIcon && ( 76 | 77 | {props.isPlaying ? props.pauseIcon : props.playIcon} 78 | 79 | )} 80 | {props.muted ? ( 81 | 82 | {props.mutedIcon} 83 | 84 | ) : ( 85 | 86 | {props.loudIcon} 87 | 88 | )} 89 | 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /packages/components/src/Video/Stage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import { ReactText } from "react" 4 | import styled from "@emotion/styled" 5 | 6 | export interface VideoStageProps { 7 | readonly muted: boolean 8 | readonly video: JSX.Element 9 | readonly controls?: JSX.Element 10 | readonly height?: ReactText 11 | 12 | handleClick(): void 13 | } 14 | 15 | interface StyledStageContainer { 16 | readonly height: ReactText 17 | } 18 | 19 | const StyledStageContainer = styled.figure` 20 | position: relative; 21 | height: ${props => props.height}; 22 | margin: 0; 23 | ` 24 | 25 | StyledStageContainer.displayName = "Video.StyledStageContainer" 26 | 27 | const StyledStageCaption = styled.figcaption` 28 | position: relative; 29 | color: ${props => props.theme.video.color}; 30 | ` 31 | 32 | StyledStageCaption.displayName = "Video.StyledStageCaption" 33 | 34 | export const Stage = ( 35 | props: React.PropsWithChildren, 36 | ): JSX.Element => ( 37 | 38 | {props.video} 39 | 40 | {props.controls} 41 | {props.children} 42 | 43 | 44 | ) 45 | -------------------------------------------------------------------------------- /packages/components/src/Video/Video.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | RefObject, 3 | forwardRef, 4 | useImperativeHandle, 5 | useRef, 6 | } from "react" 7 | 8 | import styled from "@emotion/styled" 9 | 10 | export interface VideoProps { 11 | readonly controls: boolean 12 | readonly autoPlay: boolean 13 | readonly loop: boolean 14 | readonly muted: boolean 15 | readonly preload: "auto" | "metadata" | "none" 16 | readonly sources: Source[] 17 | readonly poster?: string 18 | readonly onPlaying?: React.ReactEventHandler 19 | } 20 | 21 | export interface ImperativeRefProps { 22 | play(): Promise 23 | } 24 | 25 | export interface CustomizedChildRef { 26 | play(): Promise 27 | 28 | pause(): void 29 | } 30 | 31 | export interface Source { 32 | readonly id: string 33 | readonly src: string 34 | readonly type: "video/mp4" | "video/webm" 35 | } 36 | 37 | const StyledVideo = styled.video` 38 | width: 100%; 39 | height: 100%; 40 | object-fit: cover; 41 | ` 42 | 43 | StyledVideo.displayName = "StyledVideo" 44 | 45 | export const Media = forwardRef( 46 | (props, ref) => { 47 | const childRef = useRef() 48 | const { sources } = props 49 | 50 | useImperativeHandle(ref, () => ({ 51 | async play(): Promise { 52 | try { 53 | ;(await childRef) && childRef.current && childRef.current.play() 54 | } catch (err) { 55 | return Promise.reject(`Video play control [error]:${err}`) 56 | } 57 | }, 58 | pause(): void { 59 | childRef && childRef.current && childRef.current.pause() 60 | }, 61 | })) 62 | 63 | return sources.length > 0 ? ( 64 | }> 74 | {sources.map(source => ( 75 | 76 | ))} 77 | 78 | ) : null 79 | }, 80 | ) 81 | 82 | Media.displayName = "Video.Media" 83 | -------------------------------------------------------------------------------- /packages/components/src/Video/__tests__/Video.test.tsx: -------------------------------------------------------------------------------- 1 | import { Video, theme } from "../.." 2 | import { axe, toHaveNoViolations } from "jest-axe" 3 | import { cleanup, render } from "@testing-library/react" 4 | 5 | import React from "react" 6 | import { ThemeProvider } from "@emotion/react" 7 | import { matchers } from "@emotion/jest" 8 | 9 | expect.extend(matchers) 10 | expect.extend(toHaveNoViolations) 11 | 12 | describe("