├── .circleci └── config.yml ├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .prettierrc ├── .storybook ├── .babelrc ├── addons.js ├── config.js └── webpack.config.js ├── LICENSE ├── README.md ├── babel.config.js ├── cypress.ci.json ├── cypress.dev.json ├── cypress.json ├── cypress ├── plugins │ └── index.js └── support │ ├── commands.js │ ├── commands │ └── assertAreas.js │ └── index.js ├── examples ├── Square.jsx ├── all.test.js ├── components │ ├── Box │ │ ├── DisplayOverride.jsx │ │ └── DisplayOverride.test.js │ ├── Composition │ │ ├── declaration │ │ │ ├── GridTemplate.jsx │ │ │ ├── GridTemplate.test.js │ │ │ ├── OrderProp.jsx │ │ │ ├── OrderProp.test.js │ │ │ ├── TemplateIndentation.jsx │ │ │ ├── TemplateIndentation.test.js │ │ │ ├── TemplatePeriod.jsx │ │ │ ├── TemplatePeriod.test.js │ │ │ ├── Templateless.jsx │ │ │ └── Templateless.test.js │ │ └── rendering │ │ │ ├── NamespaceCollision.jsx │ │ │ ├── NamespaceCollision.test.js │ │ │ ├── NestedComposition.jsx │ │ │ ├── NestedComposition.test.js │ │ │ ├── WeakArea.jsx │ │ │ ├── WeakAreas.test.js │ │ │ ├── behaviors │ │ │ ├── Bell.jsx │ │ │ ├── Bell.test.js │ │ │ ├── MobileFirst.jsx │ │ │ ├── MobileFirst.test.js │ │ │ ├── Notch.jsx │ │ │ └── Notch.test.js │ │ │ └── responsive-props │ │ │ ├── BreakpointEdges.jsx │ │ │ ├── BreakpointEdges.test.js │ │ │ ├── BreakpointSpecific.jsx │ │ │ ├── BreakpointSpecific.test.js │ │ │ ├── InclusiveNotch.jsx │ │ │ ├── InclusiveNotch.test.js │ │ │ ├── MobileFirst.jsx │ │ │ └── MobileFirst.test.js │ ├── Only │ │ ├── OnlyCustomBreakpoints.jsx │ │ ├── OnlyCustomBreakpoints.test.js │ │ ├── OnlyDefaultBehavior.jsx │ │ └── OnlyDefaultBehavior.test.js │ └── Visible │ │ ├── Visible.test.js │ │ └── VisibleDefaultBehavior.jsx ├── configuration │ ├── CustomBreakpoints.jsx │ ├── CustomBreakpoints.test.js │ ├── CustomUnit.jsx │ └── CustomUnit.test.js ├── hooks │ ├── UseBreakpointChange.jsx │ ├── UseResponsiveProps.jsx │ ├── UseResponsiveValue.jsx │ ├── UseViewportChange.jsx │ ├── useBreakpointChange.test.js │ ├── useResponsiveProps.test.js │ ├── useResponsiveValue.test.js │ └── useViewportChange.test.js ├── index.js ├── recipes │ ├── IterativeAreas.jsx │ └── IterativeAreas.test.js ├── regression │ ├── DisplayNames.jsx │ ├── DisplayNames.test.js │ ├── OnlyUnmount.jsx │ ├── OnlyUnmount.test.js │ ├── ParentRerendering.jsx │ ├── ParentRerendering.test.js │ ├── SingleResponsiveProp.jsx │ ├── SingleResponsiveProp.test.js │ ├── StylesUndefined.jsx │ └── StylesUndefined.test.js ├── semantics │ ├── PolymorphicProp.jsx │ └── PolymorphicProp.test.js ├── styles.css └── utils │ ├── MakeResponsive │ ├── MakeResponsive.jsx │ └── makeResponsive.test.js │ └── Query │ ├── Query.jsx │ └── query.test.js ├── lerna.json ├── logo-full.png ├── logo.svg ├── materials ├── example-thumbnails │ ├── basic-composition.jpg │ ├── codesandbox │ │ ├── thumbnail-blue.jpg │ │ ├── thumbnail-violet.jpg │ │ └── thumbnail.jpg │ ├── conditional-rendering.jpg │ ├── custom-configuration.jpg │ ├── nested-composition.jpg │ ├── responsive-props.jpg │ └── shorthand-media-query.jpg ├── github │ ├── contrib-e2e-test.png │ ├── contrib-new-story.png │ ├── contrib-unit-test.png │ └── gitub-preview.jpg ├── react-finland-thumbnail.jpg └── react-vienna-thumbnail.jpg ├── package.json ├── packages ├── atomic-layout-core │ ├── LICENSE │ ├── README.md │ ├── global.d.ts │ ├── jest.config.js │ ├── jest.setup.js │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── Layout.spec.ts │ │ ├── Layout.ts │ │ ├── const │ │ │ ├── defaultOptions.ts │ │ │ ├── propAliases.ts │ │ │ └── props.ts │ │ ├── index.ts │ │ └── utils │ │ │ ├── breakpoints │ │ │ ├── closeBreakpoint │ │ │ │ ├── closeBreakpoint.spec.ts │ │ │ │ ├── closeBreakpoint.ts │ │ │ │ └── index.ts │ │ │ ├── getAreaRecords │ │ │ │ ├── behaviorUp.spec.ts │ │ │ │ ├── getAreaRecords.spec.ts │ │ │ │ ├── getAreaRecords.ts │ │ │ │ └── index.ts │ │ │ ├── mergeAreaRecords │ │ │ │ ├── index.ts │ │ │ │ ├── mergeAreaRecords.spec.ts │ │ │ │ └── mergeAreaRecords.ts │ │ │ ├── openBreakpoint │ │ │ │ ├── index.ts │ │ │ │ ├── openBreakpoint.spec.ts │ │ │ │ └── openBreakpoint.ts │ │ │ ├── shouldMergeBreakpoints │ │ │ │ ├── index.ts │ │ │ │ ├── shouldMergeBreakpoints.spec.ts │ │ │ │ └── shouldMergeBreakpoints.ts │ │ │ └── withBreakpoints │ │ │ │ ├── index.ts │ │ │ │ ├── matchMedia.mock.ts │ │ │ │ ├── withBreakpoints.spec.ts │ │ │ │ └── withBreakpoints.ts │ │ │ ├── functions │ │ │ ├── compose │ │ │ │ ├── compose.spec.ts │ │ │ │ ├── compose.ts │ │ │ │ └── index.ts │ │ │ ├── invariant │ │ │ │ ├── index.ts │ │ │ │ ├── invariant.spec.ts │ │ │ │ └── invariant.ts │ │ │ ├── isset │ │ │ │ ├── index.ts │ │ │ │ ├── isset.spec.ts │ │ │ │ └── isset.ts │ │ │ ├── memoizeWith │ │ │ │ ├── index.ts │ │ │ │ ├── memoizeWith.spec.ts │ │ │ │ └── memoizeWith.ts │ │ │ ├── pop │ │ │ │ ├── index.ts │ │ │ │ ├── pop.spec.ts │ │ │ │ └── pop.ts │ │ │ ├── throttle │ │ │ │ ├── index.ts │ │ │ │ ├── throttle.spec.ts │ │ │ │ └── throttle.ts │ │ │ └── warn │ │ │ │ ├── index.ts │ │ │ │ ├── warn.spec.ts │ │ │ │ └── warn.ts │ │ │ ├── math │ │ │ ├── transformNumeric.spec.ts │ │ │ └── transformNumeric.ts │ │ │ ├── strings │ │ │ ├── capitalize │ │ │ │ ├── capitalize.spec.ts │ │ │ │ ├── capitalize.ts │ │ │ │ └── index.ts │ │ │ ├── getPrefix │ │ │ │ ├── getPrefix.spec.ts │ │ │ │ ├── getPrefix.ts │ │ │ │ └── index.ts │ │ │ ├── hashString │ │ │ │ ├── hashString.spec.ts │ │ │ │ ├── hashString.ts │ │ │ │ └── index.ts │ │ │ ├── isAreaName │ │ │ │ ├── index.ts │ │ │ │ ├── isAreaName.spec.ts │ │ │ │ └── isAreaName.ts │ │ │ ├── parsePropName │ │ │ │ ├── index.ts │ │ │ │ ├── parsePropName.spec.ts │ │ │ │ └── parsePropName.ts │ │ │ ├── sanitizeTemplateArea │ │ │ │ ├── index.ts │ │ │ │ ├── sanitizeTemplateArea.spec.ts │ │ │ │ └── sanitizeTemplateArea.ts │ │ │ ├── sanitizeTemplateString │ │ │ │ ├── index.ts │ │ │ │ ├── sanitizeTemplateString.spec.ts │ │ │ │ └── sanitizeTemplateString.ts │ │ │ ├── toDashedString │ │ │ │ ├── index.ts │ │ │ │ ├── toDashedString.spec.ts │ │ │ │ └── toDashedString.ts │ │ │ └── toLowerCaseFirst │ │ │ │ ├── index.ts │ │ │ │ ├── toLowerCaseFirst.spec.ts │ │ │ │ └── toLowerCaseFirst.ts │ │ │ ├── styles │ │ │ ├── applyStyles │ │ │ │ ├── applyStyles.ts │ │ │ │ └── index.ts │ │ │ ├── createMediaQuery │ │ │ │ ├── createMediaQuery.spec.ts │ │ │ │ ├── createMediaQuery.ts │ │ │ │ └── index.ts │ │ │ └── normalizeQuery │ │ │ │ ├── index.ts │ │ │ │ ├── normalizeQuery.spec.ts │ │ │ │ └── normalizeQuery.ts │ │ │ └── templates │ │ │ ├── generateComponents │ │ │ ├── generateComponents.ts │ │ │ └── index.ts │ │ │ ├── getAreasList │ │ │ ├── getAreasList.spec.ts │ │ │ ├── getAreasList.ts │ │ │ └── index.ts │ │ │ └── parseTemplates │ │ │ ├── filterTemplateProps.spec.ts │ │ │ ├── filterTemplateProps.ts │ │ │ ├── index.ts │ │ │ └── parseTemplates.ts │ └── tsconfig.json ├── atomic-layout-emotion │ ├── LICENSE │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── logo-full.png │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ └── index.ts │ ├── test │ │ ├── refs.spec.tsx │ │ └── ssr.spec.tsx │ ├── tsconfig.json │ └── tslint.json └── atomic-layout │ ├── LICENSE │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── logo-full.png │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── components │ │ ├── Box.spec.tsx │ │ ├── Box.tsx │ │ ├── Composition.tsx │ │ ├── Only.tsx │ │ └── Visible.tsx │ ├── hooks │ │ ├── useBreakpointChange.ts │ │ ├── useMediaQuery.ts │ │ ├── useResponsiveProps.ts │ │ ├── useResponsiveQuery.ts │ │ ├── useResponsiveValue.ts │ │ └── useViewportChange.ts │ ├── index.ts │ └── utils │ │ ├── MakeResponsive.spec.tsx │ │ ├── forwardRef.ts │ │ ├── getBreakpointsByQuery.test.ts │ │ ├── getBreakpointsByQuery.ts │ │ ├── makeResponsive.tsx │ │ ├── query.test.ts │ │ ├── query.ts │ │ └── withPlaceholder.tsx │ ├── test │ ├── createForwardRefTest.tsx │ ├── createSsrTest.tsx │ ├── propAliases.spec.tsx │ ├── refs.spec.tsx │ └── ssr.spec.tsx │ ├── tsconfig.json │ └── tslint.json ├── tslint.json └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # Supported ways to fund Atomic layout efforts 2 | open_collective: atomic-layout 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | ## When: 7 | 8 | 9 | 10 | 1. Declare the following Composition: ... 11 | 2. And change the breakpoint to ... 12 | 13 | ## Current behavior: 14 | 15 | 16 | 17 | ## Expected behavior: 18 | 19 | 20 | 21 | ## Environment: 22 | 23 | - node vesrion: `0.0.0` 24 | - npm version: `0.0.0` 25 | - atomic-layout version: `0.0.0` 26 | 27 | ## Screenshots: 28 | 29 | 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | ## What: 7 | 8 | Please describe your change request suggestion. 9 | 10 | ## Why: 11 | 12 | What is the motivation for this feature/change? 13 | 14 | ## How: 15 | 16 | What is the steps needed for this feature/change? Feel free to be descriptive in technical details here. 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Changes 2 | 3 | 4 | 5 | - Changes A 6 | 7 | ## Issues 8 | 9 | 10 | 11 | - Closes #ISSUE_NUMBER 12 | 13 | ## Release version 14 | 15 | 16 | 17 | - [ ] internal (no release required) 18 | - [ ] patch (bug fixes) 19 | - [ ] minor (backward-compatible changes) 20 | - [ ] major (backward-incompatible, breaking changes) 21 | 22 | ## Contributor's checklist 23 | 24 | 25 | 26 | - [ ] My branch is up-to-date with the latest `master` 27 | 28 | 34 | 35 | - [ ] I ran `yarn verify` to see the build and tests pass 36 | 37 | 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Internal 2 | __* 3 | internal 4 | deprecated 5 | yarn-* 6 | 7 | # Modules 8 | node_modules 9 | /coverage 10 | 11 | # Build 12 | lib 13 | 14 | # Cypress 15 | cypress/fixtures 16 | cypress/screenshots 17 | cypress/videos 18 | packages/**/cypress 19 | storybook-static 20 | 21 | # Editors 22 | .idea 23 | .vscode 24 | 25 | # OS 26 | .DS_* 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Configurations 2 | .github 3 | .circleci 4 | .storybook 5 | .vscode 6 | .babelrc 7 | *.config.js 8 | .gitignore 9 | .prettierrc 10 | tsconfig.* 11 | tslint.* 12 | jest.* 13 | webpack.* 14 | yarn-* 15 | 16 | # Sources 17 | __* 18 | src 19 | internal 20 | examples 21 | stories 22 | examples 23 | /*.png 24 | materials 25 | storybook-static 26 | 27 | # Tests 28 | cypress* 29 | coverage 30 | jest.* 31 | tests 32 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "all", 4 | "jsxBracketSameLine": false, 5 | "singleQuote": true, 6 | "useTabs": false, 7 | "tabWidth": 2, 8 | "arrowParens": "always" 9 | } 10 | -------------------------------------------------------------------------------- /.storybook/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../babel.config.js" 3 | } 4 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register' 2 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { addParameters, configure } from '@storybook/react' 2 | 3 | addParameters({ 4 | options: { 5 | brandName: 'Atomic Layout', 6 | brandUrl: 'https://github.com/kettanaito/atomic-layout', 7 | }, 8 | }) 9 | 10 | configure(() => require('../examples'), module) 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Artem Zakharchenko 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. -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | modules: false, 7 | }, 8 | ], 9 | '@babel/preset-react', 10 | '@babel/preset-typescript', 11 | ], 12 | plugins: [ 13 | '@babel/plugin-proposal-class-properties', 14 | '@babel/plugin-proposal-object-rest-spread', 15 | '@babel/plugin-proposal-export-default-from', 16 | '@babel/plugin-proposal-export-namespace-from', 17 | ], 18 | env: { 19 | test: { 20 | presets: ['@babel/preset-env', '@babel/preset-react'], 21 | }, 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /cypress.ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": "{PACKAGE}/storybook-static" 3 | } 4 | -------------------------------------------------------------------------------- /cypress.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:6020" 3 | } 4 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "integrationFolder": "./examples", 3 | "testFiles": "**/*.test.js", 4 | "viewportWidth": 500 5 | } 6 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const webpackPreprocessor = require('@cypress/webpack-preprocessor') 3 | const babelConfig = require('../../babel.config') 4 | 5 | const webpackOptions = { 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.(j|t)sx?$/, 10 | exclude: /node_modules/, 11 | use: [ 12 | { 13 | loader: 'babel-loader', 14 | options: babelConfig, 15 | }, 16 | ], 17 | }, 18 | ], 19 | }, 20 | resolve: { 21 | extensions: ['.ts', '.tsx', '.spec.jsx', '.spec.js', '.jsx', '.js'], 22 | }, 23 | } 24 | 25 | const getCypressConfig = (env) => { 26 | const { envName = '', package } = env 27 | const configFilename = ['cypress', envName, 'json'].filter(Boolean).join('.') 28 | 29 | const baseConfig = require('../../cypress.json') 30 | const envConfig = envName ? require(`../../cypress.${envName}.json`) : {} 31 | 32 | if (envName) { 33 | console.log(`Extending base Cypress config with "${configFilename}"`) 34 | } 35 | 36 | const resolvedConfig = Object.assign({}, baseConfig, envConfig) 37 | 38 | if ( 39 | resolvedConfig.fileServerFolder && 40 | resolvedConfig.fileServerFolder.includes('{PACKAGE}') 41 | ) { 42 | if (package == null) { 43 | throw new Error( 44 | `Failed to run Cypress against a static file server with no "package" environmental variable specified (${package}).`, 45 | ) 46 | } 47 | 48 | const nestedServerFolder = resolvedConfig.fileServerFolder.replace( 49 | '{PACKAGE}', 50 | package, 51 | ) 52 | resolvedConfig.fileServerFolder = nestedServerFolder 53 | 54 | console.log(`Loading test suites at "${nestedServerFolder}"`) 55 | } 56 | 57 | console.log('Resolved Cypress config:') 58 | console.log(JSON.stringify(resolvedConfig, null, 2)) 59 | 60 | return resolvedConfig 61 | } 62 | 63 | module.exports = (on, config) => { 64 | on( 65 | 'file:preprocessor', 66 | webpackPreprocessor({ 67 | webpackOptions, 68 | watchOptions: {}, 69 | }), 70 | ) 71 | 72 | return getCypressConfig(config.env) 73 | } 74 | -------------------------------------------------------------------------------- /cypress/support/commands/assertAreas.js: -------------------------------------------------------------------------------- 1 | const assertAxis = (areas, areaName, getArea, createSelector, areaPosition) => { 2 | const [rowIndex, columnIndex] = areaPosition 3 | 4 | const existingAreasOtherThan = (areaName) => (siblingAreaName) => { 5 | return !!siblingAreaName && siblingAreaName !== areaName 6 | } 7 | 8 | // Assert same Y axis with the areas in the same row 9 | const rowSiblings = areas[rowIndex].filter(existingAreasOtherThan(areaName)) 10 | 11 | rowSiblings.map((siblingAreaName) => { 12 | getArea(areaName) 13 | .haveSameAxis('y', createSelector(siblingAreaName)) 14 | .notIntersectWith(createSelector(siblingAreaName)) 15 | }) 16 | 17 | // Assert same X axis with the areas in the same column 18 | const columnSiblings = areas 19 | .reduce((acc, row) => { 20 | return acc.concat(row[columnIndex]) 21 | }, []) 22 | .filter(existingAreasOtherThan(areaName)) 23 | 24 | columnSiblings.forEach((siblingAreaName) => { 25 | getArea(areaName) 26 | .haveSameAxis('x', createSelector(siblingAreaName)) 27 | .notIntersectWith(createSelector(siblingAreaName)) 28 | }) 29 | } 30 | 31 | export default function assertAreas(areas, context = null) { 32 | const createSelector = (areaName) => { 33 | return [context, `[data-area="${areaName}"]`].filter(Boolean).join(' ') 34 | } 35 | const getArea = (areaName) => cy.get(createSelector(areaName)) 36 | 37 | areas.forEach((row, rowIndex) => { 38 | row.forEach((areaName, columnIndex) => { 39 | // Bypass "false" or missing areas 40 | if (!areaName) { 41 | return 42 | } 43 | 44 | // Assert that area is displayed on the page 45 | cy.log(`Assert area "${areaName}" is visible`) 46 | getArea(areaName).should('be.visible') 47 | 48 | // Assert that area has proper "grid-area" CSS property 49 | cy.log(`Assert area "${areaName}" has "grid-area:${areaName}"`) 50 | getArea(areaName).haveArea(areaName) 51 | 52 | // Assert area's axis relation and intersection with 53 | // the siblings in the same row and column. 54 | assertAxis(areas, areaName, getArea, createSelector, [ 55 | rowIndex, 56 | columnIndex, 57 | ]) 58 | }) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | import './commands' 2 | -------------------------------------------------------------------------------- /examples/Square.jsx: -------------------------------------------------------------------------------- 1 | import styled from 'supported-styling-library' 2 | 3 | const Square = styled.span` 4 | box-sizing: border-box; 5 | 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | 10 | min-height: 40px; 11 | height: ${({ height }) => height || '100%'}; 12 | width: ${({ width }) => width || '100%'}; 13 | background-color: hsla(235, 90%, 72%, 0.25); 14 | border: 1px solid hsla(235, 50%, 75%, 0.35); 15 | border-radius: 3px; 16 | color: hsla(235, 25%, 50%); 17 | font-size: 90%; 18 | font-weight: bold; 19 | text-transform: uppercase; 20 | ` 21 | 22 | export default Square 23 | -------------------------------------------------------------------------------- /examples/components/Box/DisplayOverride.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'supported-styling-library' 3 | import { Box } from 'atomic-layout' 4 | 5 | const StyledBox = styled.div` 6 | display: table; 7 | ` 8 | 9 | const BoxDisplayOverride = () => { 10 | return ( 11 | 12 | I should have display: table 13 | 14 | ) 15 | } 16 | 17 | export default BoxDisplayOverride 18 | -------------------------------------------------------------------------------- /examples/components/Box/DisplayOverride.test.js: -------------------------------------------------------------------------------- 1 | it('Supports CSS properties overrides', () => { 2 | cy.loadStory(['components', 'box'], ['properties-override']) 3 | 4 | cy.get('#box') 5 | .should('be.visible') 6 | .should('have.css', 'display', 'table') 7 | }) 8 | -------------------------------------------------------------------------------- /examples/components/Composition/declaration/GridTemplate.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const templateMobile = ` 6 | header 100px 7 | content 1fr 8 | footer auto 9 | ` 10 | 11 | const templateTablet = ` 12 | header content footer 500px 13 | / 200px 1fr auto 14 | ` 15 | 16 | const List = () => ( 17 | 23 | {({ Header, Content, Footer }) => ( 24 | <> 25 | 28 | 29 | Content 30 | 31 | 36 | 37 | )} 38 | 39 | ) 40 | 41 | export default List 42 | -------------------------------------------------------------------------------- /examples/components/Composition/declaration/GridTemplate.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | describe('Grid template', function() { 4 | before(() => { 5 | cy.loadStory( 6 | ['components', 'composition', 'declaration'], 7 | ['grid-template-syntax'], 8 | ) 9 | }) 10 | 11 | beforeEach(() => { 12 | cy.setBreakpoint('xs') 13 | }) 14 | 15 | it('Areas should not intersect', () => { 16 | cy.get('#header').notIntersectWith('#content') 17 | cy.get('#content').notIntersectWith('#footer') 18 | 19 | cy.setBreakpoint('md').then(() => { 20 | cy.get('#header').notIntersectWith('#content') 21 | cy.get('#content').notIntersectWith('#footer') 22 | }) 23 | }) 24 | 25 | describe('row', () => { 26 | it('Supports "auto" as the row height', () => { 27 | cy.get('#footer').should('have.css', 'height', '70px') 28 | }) 29 | 30 | it('Supports exact row height', () => { 31 | cy.get('#header').should('have.css', 'height', '100px') 32 | 33 | cy.setBreakpoint('md').then(() => { 34 | cy.get('#header').should('have.css', 'height', '500px') 35 | cy.get('#content').should('have.css', 'height', '500px') 36 | }) 37 | }) 38 | 39 | it('Supports dynamic row height', () => { 40 | cy.get('#content').should('have.css', 'height', '40px') 41 | }) 42 | }) 43 | 44 | describe('column', () => { 45 | it('Supports "auto" as a column width', () => { 46 | cy.setBreakpoint('md').then(() => { 47 | cy.get('#footer').should('have.css', 'width', '50px') 48 | }) 49 | }) 50 | 51 | it('Supports exact column width', () => { 52 | cy.setBreakpoint('md').then(() => { 53 | cy.get('#header').should('have.css', 'width', '200px') 54 | }) 55 | }) 56 | 57 | it('Supports dynamic column width', () => { 58 | cy.setBreakpoint('md').then(() => { 59 | cy.get('#composition').then(([parent]) => { 60 | const { clientWidth: parentWidth } = parent 61 | const { header, content, footer } = parent.children 62 | const { clientWidth: headerWidth } = header 63 | const { clientWidth: contentWidth } = content 64 | const { clientWidth: footerWidth } = footer 65 | 66 | expect(contentWidth).to.equal( 67 | parentWidth - headerWidth - footerWidth - 10 * 2, 68 | ) 69 | }) 70 | }) 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /examples/components/Composition/declaration/OrderProp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition, Box } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const OrderPropExample = () => { 6 | return ( 7 | <> 8 |

Template-less composition

9 |

10 | Template-less Composition should respect a custom order{' '} 11 | prop of its children. 12 |

13 | 14 | 15 | First 16 | 17 | 18 | Second 19 | 20 | 21 | Third 22 | 23 | 24 | 25 |

Regular composition

26 |

27 | When given explicit areas/template prop, 28 | Composition should ignore any given order prop on its 29 | children areas, and always render according to the template. 30 |

31 | 32 | {(Areas) => ( 33 | <> 34 | 35 | Left 36 | 37 | 38 | Center 39 | 40 | 41 | Right 42 | 43 | 44 | )} 45 | 46 | 47 | ) 48 | } 49 | 50 | export default OrderPropExample 51 | -------------------------------------------------------------------------------- /examples/components/Composition/declaration/OrderProp.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | describe('Order prop', function() { 4 | before(() => { 5 | cy.loadStory(['components', 'composition', 'declaration'], ['order-prop']) 6 | }) 7 | 8 | describe('Template-less composition', () => { 9 | it('Renders the child with negative "order" first', () => { 10 | cy.get('#box-third').then(([thirdElement]) => { 11 | cy.get('#box-first').then(([firstElement]) => { 12 | expect(thirdElement.offsetTop).to.be.lessThan(firstElement.offsetTop) 13 | }) 14 | }) 15 | }) 16 | 17 | it('Renders other children as-is', () => { 18 | cy.get('#box-first').then(([firstElement]) => { 19 | cy.get('#box-second').then(([secondElement]) => { 20 | expect(firstElement.offsetTop).to.be.lessThan(secondElement.offsetTop) 21 | }) 22 | }) 23 | }) 24 | }) 25 | 26 | describe('Composition with a template', () => { 27 | it('Ignores the "order" prop on area component', () => { 28 | cy.get('[data-area="right"]').then(([rightElement]) => { 29 | cy.get('[data-area="left"]').then(([leftElement]) => { 30 | expect(rightElement.offsetLeft).to.be.greaterThan( 31 | leftElement.offsetLeft, 32 | ) 33 | }) 34 | }) 35 | }) 36 | 37 | it('Renders areas according to the template', () => { 38 | cy.get('#regular-composition').assertAreas([['left', 'center', 'right']]) 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /examples/components/Composition/declaration/TemplateIndentation.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const unindentedAreas = ` 6 | first 7 | second 8 | ` 9 | 10 | const terriblyIndentedAreas = ` 11 | third 12 | fourth 13 | ` 14 | 15 | const TemplateIndentation = () => ( 16 | <> 17 | 18 | {({ First, Second }) => ( 19 | <> 20 | 21 | First 22 | 23 | 24 | Second 25 | 26 | 27 | )} 28 | 29 | 30 | 31 | {({ Third, Fourth }) => ( 32 | <> 33 | 34 | Third 35 | 36 | 37 | Fourh 38 | 39 | 40 | )} 41 | 42 | 43 | ) 44 | 45 | export default TemplateIndentation 46 | -------------------------------------------------------------------------------- /examples/components/Composition/declaration/TemplateIndentation.test.js: -------------------------------------------------------------------------------- 1 | describe('Template string indentation', () => { 2 | before(() => { 3 | cy.loadStory( 4 | ['components', 'composition', 'declaration'], 5 | ['template-indentation'], 6 | ) 7 | }) 8 | 9 | it('Supports unindented template string', () => { 10 | cy.get('#first') 11 | .should('be.visible') 12 | .haveArea('first') 13 | .notIntersectWith('#second') 14 | 15 | cy.get('#second') 16 | .should('be.visible') 17 | .haveArea('second') 18 | }) 19 | 20 | it('Supports mis-indented template string', () => { 21 | cy.get('#third') 22 | .should('be.visible') 23 | .haveArea('third') 24 | .notIntersectWith('#fourth') 25 | 26 | cy.get('#fourth') 27 | .should('be.visible') 28 | .haveArea('fourth') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /examples/components/Composition/declaration/TemplatePeriod.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const areas = ` 6 | left . right 7 | ` 8 | 9 | const TemplatePeriod = () => ( 10 | 11 | {({ Left, Right }) => ( 12 | <> 13 | 14 | Left 15 | 16 | 17 | Right 18 | 19 | 20 | )} 21 | 22 | ) 23 | 24 | export default TemplatePeriod 25 | -------------------------------------------------------------------------------- /examples/components/Composition/declaration/TemplatePeriod.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | 3 | describe('Template period', () => { 4 | before(() => { 5 | cy.loadStory( 6 | ['components', 'composition', 'declaration'], 7 | ['template-period'], 8 | ) 9 | }) 10 | 11 | it('Renders named areas according to the template string', () => { 12 | cy.get('#left') 13 | .should('be.visible') 14 | .haveArea('left') 15 | .notIntersectWith('#right') 16 | cy.get('#right') 17 | .should('be.visible') 18 | .haveArea('right') 19 | }) 20 | 21 | it('Renders dot (".") placeholder where specified', () => { 22 | cy.get('#left').then((leftAreaWrapper) => { 23 | cy.get('#right').then((rightAreaWrapper) => { 24 | const [leftArea] = leftAreaWrapper 25 | const [rightArea] = rightAreaWrapper 26 | 27 | expect(rightArea.offsetLeft).to.be.gt( 28 | leftArea.clientLeft + leftArea.offsetWidth + 20, 29 | ) 30 | }) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /examples/components/Composition/declaration/Templateless.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const TemplatelessComposition = () => ( 6 | 7 | <> 8 | First 9 | Second 10 | Third 11 | 12 | 13 | ) 14 | 15 | export default TemplatelessComposition 16 | -------------------------------------------------------------------------------- /examples/components/Composition/declaration/Templateless.test.js: -------------------------------------------------------------------------------- 1 | describe('Template-less composition', () => { 2 | before(() => { 3 | cy.loadStory( 4 | ['components', 'composition', 'declaration'], 5 | ['template-less-composition'], 6 | ) 7 | }) 8 | 9 | it('Renders children without crashing', () => { 10 | cy.get('#first').should('have.css', 'width', '200px') 11 | cy.get('#second').should('have.css', 'width', '300px') 12 | cy.get('#third').should('have.css', 'width', '400px') 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/NamespaceCollision.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | 4 | const Logo = () => Logo 5 | const Menu = () => Menu 6 | 7 | const Namespaces = () => ( 8 | 9 | {(Areas) => ( 10 | <> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | )} 19 | 20 | ) 21 | 22 | export default Namespaces 23 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/NamespaceCollision.test.js: -------------------------------------------------------------------------------- 1 | describe('Namespace collision', () => { 2 | before(() => { 3 | cy.loadStory( 4 | ['components', 'composition', 'rendering'], 5 | ['namespace-collision'], 6 | ) 7 | }) 8 | 9 | it('Renders children without crashing', () => { 10 | cy.get('#composition').assertAreas([['logo', 'menu']]) 11 | }) 12 | 13 | it('Renders custom components without namespace collision', () => { 14 | cy.get('[data-area="logo"]').should('have.text', 'Logo') 15 | cy.get('[data-area="menu"]').should('have.text', 'Menu') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/NestedComposition.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Bell from './behaviors/Bell' 3 | import Notch from './behaviors/Notch' 4 | 5 | const NestedComposition = () => ( 6 | 7 | 8 | 9 | ) 10 | 11 | export default NestedComposition 12 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/NestedComposition.test.js: -------------------------------------------------------------------------------- 1 | describe('Nested composition', () => { 2 | before(() => { 3 | cy.loadStory( 4 | ['components', 'composition', 'rendering'], 5 | ['nested-composition'], 6 | ) 7 | }) 8 | 9 | it('Parent composition behaves as bell', () => { 10 | cy.get('#notch').assertNotch() 11 | }) 12 | 13 | it('Child composite behaves as notch', () => { 14 | cy.get('#bell').assertBell() 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/WeakArea.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const WeakAreaRendering = () => ( 6 | 12 | {({ Left, Center, Right, Extra }) => ( 13 | <> 14 | 15 | Left 16 | 17 | {/* Area present only in some definitions */} 18 |
19 | Center 20 |
21 | 22 | Right 23 | 24 | {/* Undefined area component */} 25 | Foo 26 | 27 | )} 28 |
29 | ) 30 | 31 | export default WeakAreaRendering 32 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/WeakAreas.test.js: -------------------------------------------------------------------------------- 1 | describe('Weak areas', () => { 2 | before(() => { 3 | cy.loadStory(['components', 'composition', 'rendering'], ['weak-areas']) 4 | }) 5 | 6 | describe('On extra-small screen', () => { 7 | before(() => { 8 | cy.setBreakpoint('sm') 9 | }) 10 | 11 | it('Renders existing areas', () => { 12 | cy.get('#composition').assertAreas([['left', 'right']]) 13 | }) 14 | 15 | it('Does not render weak area', () => { 16 | cy.get('#center').should('not.exist') 17 | }) 18 | 19 | it('Does not render undefined area', () => { 20 | cy.get('#extra').should('not.exist') 21 | }) 22 | }) 23 | 24 | describe('On medium screen', () => { 25 | before(() => { 26 | cy.setBreakpoint('md') 27 | }) 28 | 29 | it('Renders all areas', () => { 30 | cy.get('#composition').assertAreas([['left', 'center', 'right']]) 31 | }) 32 | 33 | it('Does not render undefined area', () => { 34 | cy.get('#extra').should('not.exist') 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/behaviors/Bell.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const template = 'first second' 6 | const templateSm = 'first second third' 7 | const templateLg = 'first second' 8 | 9 | const BellBehavior = ({ children }) => ( 10 | 17 | {({ First, Second, Third }) => ( 18 | <> 19 | 20 | {children || 'First'} 21 | 22 | 23 | Second 24 | 25 | 26 | Third 27 | 28 | 29 | )} 30 | 31 | ) 32 | 33 | export default BellBehavior 34 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/behaviors/Bell.test.js: -------------------------------------------------------------------------------- 1 | it('Bell', () => { 2 | cy.loadStory( 3 | ['components', 'composition', 'rendering', 'behaviors'], 4 | ['bell'], 5 | ) 6 | 7 | cy.get('#bell').assertBell() 8 | }) 9 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/behaviors/MobileFirst.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const Scenario = () => ( 6 | 7 | {({ Left, Center, Right }) => ( 8 | <> 9 | 10 | Left 11 | 12 |
13 | Center 14 |
15 | 16 | Right 17 | 18 | 19 | )} 20 |
21 | ) 22 | 23 | export default Scenario 24 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/behaviors/MobileFirst.test.js: -------------------------------------------------------------------------------- 1 | it('Mobile first', () => { 2 | cy.loadStory( 3 | ['components', 'composition', 'rendering', 'behaviors'], 4 | ['mobile-first'], 5 | ) 6 | 7 | const assertInline = () => { 8 | cy.get('#composition').assertAreas([['left', 'center', 'right']]) 9 | } 10 | 11 | assertInline() 12 | cy.setBreakpoint('sm').then(assertInline) 13 | cy.setBreakpoint('md').then(assertInline) 14 | cy.setBreakpoint('lg').then(assertInline) 15 | cy.setBreakpoint('xl').then(assertInline) 16 | }) 17 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/behaviors/Notch.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const template = 'left center right' 6 | const templateMd = 'left right' 7 | const templateLg = 'left center right' 8 | 9 | const Scenario = () => ( 10 | 17 | {({ Left, Center, Right }) => ( 18 | <> 19 | 20 | Left 21 | 22 |
23 | Center 24 |
25 | 26 | Right 27 | 28 | 29 | )} 30 |
31 | ) 32 | 33 | export default Scenario 34 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/behaviors/Notch.test.js: -------------------------------------------------------------------------------- 1 | it('Notch', () => { 2 | cy.loadStory( 3 | ['components', 'composition', 'rendering', 'behaviors'], 4 | ['notch'], 5 | ) 6 | 7 | cy.get('#notch').assertNotch() 8 | }) 9 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/responsive-props/BreakpointEdges.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const BreakpointEdges = () => ( 6 | 15 | Left 16 | Right 17 | 18 | ) 19 | 20 | export default BreakpointEdges 21 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/responsive-props/BreakpointEdges.test.js: -------------------------------------------------------------------------------- 1 | describe('Breakpoint edges', () => { 2 | before(() => { 3 | cy.loadStory( 4 | ['components', 'composition', 'rendering', 'responsive-props'], 5 | ['breakpoint-edges'], 6 | ) 7 | }) 8 | 9 | const assertMaxWidth = (value) => { 10 | return cy.get('#composition').should('have.css', 'max-width', value) 11 | } 12 | 13 | it('Renders correctly on initial render', () => { 14 | assertMaxWidth('200px') 15 | }) 16 | 17 | it('Renders correctly at a breakpoint', () => { 18 | cy.setBreakpoint('sm').then(() => { 19 | assertMaxWidth('400px') 20 | }) 21 | cy.setBreakpoint('md').then(() => { 22 | assertMaxWidth('400px') 23 | }) 24 | cy.setBreakpoint('lg').then(() => { 25 | assertMaxWidth('100%') 26 | }) 27 | cy.setBreakpoint('xl').then(() => { 28 | assertMaxWidth('100%') 29 | }) 30 | }) 31 | 32 | it('Renders correctly within a breakpoint', () => { 33 | // xs 34 | cy.setBreakpoint({ minWidth: 550 }).then(() => { 35 | assertMaxWidth('200px') 36 | }) 37 | 38 | //sm 39 | cy.setBreakpoint({ minWidth: 650 }).then(() => { 40 | assertMaxWidth('400px') 41 | }) 42 | 43 | // md 44 | cy.setBreakpoint({ minWidth: 850 }).then(() => { 45 | assertMaxWidth('400px') 46 | }) 47 | 48 | // lg 49 | cy.setBreakpoint({ minWidth: 1050 }).then(() => { 50 | assertMaxWidth('100%') 51 | }) 52 | 53 | // xl 54 | cy.setBreakpoint({ minWidth: 1400 }).then(() => { 55 | assertMaxWidth('100%') 56 | }) 57 | }) 58 | 59 | it('Renders correctly on the trailing edge', () => { 60 | // xs 61 | cy.setBreakpoint({ minWidth: 575 }).then(() => { 62 | assertMaxWidth('200px') 63 | }) 64 | 65 | // sm 66 | cy.setBreakpoint({ minWidth: 576 }).then(() => { 67 | assertMaxWidth('400px') 68 | }) 69 | cy.setBreakpoint({ minWidth: 767 }).then(() => { 70 | assertMaxWidth('400px') 71 | }) 72 | 73 | // md 74 | cy.setBreakpoint({ minWidth: 768 }).then(() => { 75 | assertMaxWidth('400px') 76 | }) 77 | cy.setBreakpoint({ minWidth: 991 }).then(() => { 78 | assertMaxWidth('400px') 79 | }) 80 | 81 | // lg 82 | cy.setBreakpoint({ minWidth: 992 }).then(() => { 83 | assertMaxWidth('100%') 84 | }) 85 | cy.setBreakpoint({ minWidth: 1199 }).then(() => { 86 | assertMaxWidth('100%') 87 | }) 88 | 89 | // xl 90 | cy.setBreakpoint({ minWidth: 1200 }).then(() => { 91 | assertMaxWidth('100%') 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/responsive-props/BreakpointSpecific.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const BreakpointSpecific = () => ( 6 | 15 | {({ First, Second }) => ( 16 | <> 17 | 18 | First 19 | 20 | 21 | Second 22 | 23 | 24 | )} 25 | 26 | ) 27 | 28 | export default BreakpointSpecific 29 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/responsive-props/BreakpointSpecific.test.js: -------------------------------------------------------------------------------- 1 | it('Breakpoint-specific', () => { 2 | cy.loadStory( 3 | ['components', 'composition', 'rendering', 'responsive-props'], 4 | ['breakpoint-specific'], 5 | ) 6 | 7 | const assertGutter = (values) => { 8 | return cy.get('#composition').should('have.css', 'grid-gap', values) 9 | } 10 | 11 | assertGutter('10px 10px') 12 | cy.setBreakpoint('sm').then(() => assertGutter('20px 20px')) 13 | cy.setBreakpoint('md').then(() => assertGutter('30px 30px')) 14 | cy.setBreakpoint('lg').then(() => assertGutter('40px 40px')) 15 | cy.setBreakpoint('xl').then(() => assertGutter('50px 50px')) 16 | }) 17 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/responsive-props/InclusiveNotch.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const template = 'first second' 6 | 7 | const InclusiveNotch = () => ( 8 | <> 9 | 16 | {({ First, Second }) => ( 17 | <> 18 | 19 | First 20 | 21 | 22 | Second 23 | 24 | 25 | )} 26 | 27 | 28 | 34 | {({ First, Second }) => ( 35 | <> 36 | 37 | First 38 | 39 | 40 | Second 41 | 42 | 43 | )} 44 | 45 | 46 | ) 47 | 48 | export default InclusiveNotch 49 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/responsive-props/InclusiveNotch.test.js: -------------------------------------------------------------------------------- 1 | it('Bell & Notch', () => { 2 | cy.loadStory( 3 | ['components', 'composition', 'rendering', 'responsive-props'], 4 | ['inclusive-notch'], 5 | ) 6 | 7 | const assertPadding = (selector, values) => { 8 | return cy.get(selector).should('have.css', 'padding', values) 9 | } 10 | 11 | assertPadding('#composition-one', '10px') 12 | cy.setBreakpoint('sm').then(() => assertPadding('#composition-one', '20px')) 13 | cy.setBreakpoint('md').then(() => assertPadding('#composition-one', '20px')) 14 | cy.setBreakpoint('lg').then(() => assertPadding('#composition-one', '20px')) 15 | cy.setBreakpoint('xl').then(() => assertPadding('#composition-one', '10px')) 16 | 17 | assertPadding('#composition-two', '10px') 18 | cy.setBreakpoint('sm').then(() => assertPadding('#composition-two', '10px')) 19 | cy.setBreakpoint('md').then(() => assertPadding('#composition-two', '20px')) 20 | cy.setBreakpoint('lg').then(() => assertPadding('#composition-two', '10px')) 21 | cy.setBreakpoint('xl').then(() => assertPadding('#composition-two', '10px')) 22 | }) 23 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/responsive-props/MobileFirst.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const Foo = () => ( 6 | 7 | {({ First, Second }) => ( 8 | <> 9 | 10 | First 11 | 12 | 13 | Second 14 | 15 | 16 | )} 17 | 18 | ) 19 | 20 | export default Foo 21 | -------------------------------------------------------------------------------- /examples/components/Composition/rendering/responsive-props/MobileFirst.test.js: -------------------------------------------------------------------------------- 1 | it('Mobile first', () => { 2 | cy.loadStory( 3 | ['components', 'composition', 'rendering', 'responsive-props'], 4 | ['mobile-first'], 5 | ) 6 | 7 | const assertGutter = () => { 8 | return cy.get('#composition').should('have.css', 'grid-gap', '10px 10px') 9 | } 10 | 11 | assertGutter() 12 | cy.setBreakpoint('sm').then(assertGutter) 13 | cy.setBreakpoint('md').then(assertGutter) 14 | cy.setBreakpoint('lg').then(assertGutter) 15 | cy.setBreakpoint('xl').then(assertGutter) 16 | }) 17 | -------------------------------------------------------------------------------- /examples/components/Only/OnlyCustomBreakpoints.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Only } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | export const breakpoints = { 6 | mobile: { 7 | minWidth: 500, 8 | maxWidth: 600, 9 | }, 10 | largeMobile: { 11 | minWidth: 601, 12 | maxWidth: 700, 13 | }, 14 | tablet: { 15 | minWidth: 701, 16 | maxWidth: 800, 17 | }, 18 | desktop: { 19 | minWidth: 801, 20 | maxWidth: 1200, 21 | }, 22 | } 23 | 24 | const OnlyCustomBreakpoint = () => ( 25 |
26 | {/* Exact breakpoint */} 27 | 28 | First 29 | 30 | 31 | {/* High-pass */} 32 | 33 | Second 34 | 35 | 36 | {/* Low-pass */} 37 | 38 | Third 39 | 40 | 41 | {/* Bell */} 42 | 43 | Fourth 44 | 45 | 46 | {/* Notch */} 47 | 48 | Fifth 49 | 50 |
51 | ) 52 | 53 | export default OnlyCustomBreakpoint 54 | -------------------------------------------------------------------------------- /examples/components/Only/OnlyCustomBreakpoints.test.js: -------------------------------------------------------------------------------- 1 | describe('Supports custom breakpoints', () => { 2 | before(() => { 3 | cy.loadStory(['components', 'only'], ['custom-breakpoints']) 4 | }) 5 | 6 | it('Renders children at the exact breakpoint (for)', () => { 7 | cy.setBreakpoint({ minWidth: 500 }).then(() => cy.shouldRender('#first')) 8 | cy.setBreakpoint({ minWidth: 601 }).then(() => 9 | cy.shouldRender('#first', false), 10 | ) 11 | cy.setBreakpoint({ minWidth: 701 }).then(() => 12 | cy.shouldRender('#first', false), 13 | ) 14 | cy.setBreakpoint({ minWidth: 801 }).then(() => 15 | cy.shouldRender('#first', false), 16 | ) 17 | }) 18 | 19 | it('Renders children up to a given breakpoint (to)', () => { 20 | cy.setBreakpoint({ minWidth: 500 }).then(() => cy.shouldRender('#second')) 21 | cy.setBreakpoint({ minWidth: 601 }).then(() => cy.shouldRender('#second')) 22 | cy.setBreakpoint({ minWidth: 701 }).then(() => 23 | cy.shouldRender('#second', false), 24 | ) 25 | cy.setBreakpoint({ minWidth: 801 }).then(() => 26 | cy.shouldRender('#second', false), 27 | ) 28 | }) 29 | 30 | it('Renders children from a given breakpoint (from)', () => { 31 | cy.setBreakpoint({ minWidth: 500 }).then(() => 32 | cy.shouldRender('#third', false), 33 | ) 34 | cy.setBreakpoint({ minWidth: 601 }).then(() => 35 | cy.shouldRender('#third', false), 36 | ) 37 | cy.setBreakpoint({ minWidth: 701 }).then(() => cy.shouldRender('#third')) 38 | cy.setBreakpoint({ minWidth: 801 }).then(() => cy.shouldRender('#third')) 39 | }) 40 | 41 | it('Renders children within a breakpoint range (from/to)', () => { 42 | cy.setBreakpoint({ minWidth: 500 }).then(() => 43 | cy.shouldRender('#fourth', false), 44 | ) 45 | cy.setBreakpoint({ minWidth: 601 }).then(() => cy.shouldRender('#fourth')) 46 | cy.setBreakpoint({ minWidth: 701 }).then(() => cy.shouldRender('#fourth')) 47 | cy.setBreakpoint({ minWidth: 801 }).then(() => 48 | cy.shouldRender('#fourth', false), 49 | ) 50 | }) 51 | 52 | it('Renders children excluding a breakpoint range (except)', () => { 53 | cy.setBreakpoint({ minWidth: 500 }).then(() => cy.shouldRender('#fifth')) 54 | cy.setBreakpoint({ minWidth: 601 }).then(() => 55 | cy.shouldRender('#fifth', false), 56 | ) 57 | cy.setBreakpoint({ minWidth: 701 }).then(() => 58 | cy.shouldRender('#fifth', false), 59 | ) 60 | cy.setBreakpoint({ minWidth: 801 }).then(() => cy.shouldRender('#fifth')) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /examples/components/Only/OnlyDefaultBehavior.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Only } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const OnlyDefault = () => ( 6 | <> 7 | {/* Exact breakpoint */} 8 | 9 | For "sm" 10 | 11 | 12 | {/* High-pass */} 13 | 14 | To "md" 15 | 16 | 17 | {/* Low-pass */} 18 | 19 | From "lg" 20 | 21 | 22 | {/* Bell */} 23 | 24 | From "sm" to "lg" 25 | 26 | 27 | {/* Notch */} 28 | 29 | Except from "md" to "lg" 30 | 31 | 32 | ) 33 | 34 | export default OnlyDefault 35 | -------------------------------------------------------------------------------- /examples/components/Only/OnlyDefaultBehavior.test.js: -------------------------------------------------------------------------------- 1 | describe('Default behavior', () => { 2 | before(() => { 3 | cy.loadStory(['components', 'only'], ['default-behavior']) 4 | }) 5 | 6 | it('Renders children at the exact breakpoint (for)', () => { 7 | cy.setBreakpoint('xs').then(() => cy.shouldRender('#first', false)) 8 | cy.setBreakpoint('sm').then(() => cy.shouldRender('#first')) 9 | cy.setBreakpoint('md').then(() => cy.shouldRender('#first', false)) 10 | cy.setBreakpoint('lg').then(() => cy.shouldRender('#first', false)) 11 | cy.setBreakpoint('xl').then(() => cy.shouldRender('#first', false)) 12 | }) 13 | 14 | it('Renders children up to a given breakpoint (to)', () => { 15 | cy.setBreakpoint('xs').then(() => cy.shouldRender('#second')) 16 | cy.setBreakpoint('sm').then(() => cy.shouldRender('#second')) 17 | cy.setBreakpoint('md').then(() => cy.shouldRender('#second', false)) 18 | cy.setBreakpoint('lg').then(() => cy.shouldRender('#second', false)) 19 | cy.setBreakpoint('xl').then(() => cy.shouldRender('#second', false)) 20 | }) 21 | 22 | it('Renders children from a given breakpoint (from)', () => { 23 | cy.setBreakpoint('xs').then(() => cy.shouldRender('#third', false)) 24 | cy.setBreakpoint('sm').then(() => cy.shouldRender('#third', false)) 25 | cy.setBreakpoint('md').then(() => cy.shouldRender('#third', false)) 26 | cy.setBreakpoint('lg').then(() => cy.shouldRender('#third')) 27 | cy.setBreakpoint('xl').then(() => cy.shouldRender('#third')) 28 | }) 29 | 30 | it('Renders children within a breakpoint range (from/to)', () => { 31 | cy.setBreakpoint('xs').then(() => cy.shouldRender('#fourth', false)) 32 | cy.setBreakpoint('sm').then(() => cy.shouldRender('#fourth')) 33 | cy.setBreakpoint('md').then(() => cy.shouldRender('#fourth')) 34 | cy.setBreakpoint('lg').then(() => cy.shouldRender('#fourth', false)) 35 | cy.setBreakpoint('xl').then(() => cy.shouldRender('#fourth', false)) 36 | }) 37 | 38 | it('Renders children excluding a breakpoint range (except)', () => { 39 | cy.setBreakpoint('xs').then(() => cy.shouldRender('#fifth')) 40 | cy.setBreakpoint('sm').then(() => cy.shouldRender('#fifth')) 41 | cy.setBreakpoint('md').then(() => cy.shouldRender('#fifth', false)) 42 | cy.setBreakpoint('lg').then(() => cy.shouldRender('#fifth')) 43 | cy.setBreakpoint('xl').then(() => cy.shouldRender('#fifth')) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /examples/components/Visible/VisibleDefaultBehavior.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Visible } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const VisibleDefaultBehavior = () => ( 6 | <> 7 | {/* Explicit breakpoint */} 8 | 9 | For "sm" 10 | 11 | {/* High-pass */} 12 | 13 | To "md" 14 | 15 | {/* Low-pass */} 16 | 17 | From "lg" 18 | 19 | {/* Bell */} 20 | 21 | From "sm" to "lg" 22 | 23 | {/* Notch */} 24 | 25 | Except from "md" to "lg" 26 | 27 | 28 | ) 29 | 30 | export default VisibleDefaultBehavior 31 | -------------------------------------------------------------------------------- /examples/configuration/CustomBreakpoints.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Layout, { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const areas = ` 6 | first 7 | second 8 | ` 9 | 10 | export default class CustomBreakpoints extends React.Component { 11 | componentWillMount() { 12 | Layout.configure({ 13 | defaultUnit: 'rem', 14 | defaultBreakpointName: 'mobile', 15 | breakpoints: { 16 | mobile: { 17 | maxWidth: '575px', 18 | }, 19 | tablet: { 20 | minWidth: '576px', 21 | maxWidth: '768px', 22 | }, 23 | desktop: { 24 | minWidth: '769px', 25 | }, 26 | }, 27 | }) 28 | } 29 | 30 | render() { 31 | return ( 32 | 40 | {({ First, Second }) => ( 41 | <> 42 | 43 | First 44 | 45 | 46 | Second 47 | 48 | 49 | )} 50 | 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/configuration/CustomBreakpoints.test.js: -------------------------------------------------------------------------------- 1 | it('Supports custom breakpoints', () => { 2 | cy.loadStory( 3 | ['configuration', 'custom-configuration'], 4 | ['custom-breakpoints'], 5 | ) 6 | 7 | const assertPadding = (values) => { 8 | return cy.get('#composition').should('have.css', 'padding', values) 9 | } 10 | 11 | assertPadding('16px') 12 | 13 | cy.setBreakpoint({ 14 | minWidth: 576, 15 | maxWidth: 768, 16 | }).then(() => assertPadding('32px')) 17 | 18 | cy.setBreakpoint({ 19 | minWidth: 769, 20 | }).then(() => assertPadding('48px')) 21 | }) 22 | -------------------------------------------------------------------------------- /examples/configuration/CustomUnit.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Layout from 'atomic-layout' 3 | import Notch from '../components/Composition/rendering/behaviors/Notch' 4 | 5 | export default class CustomUnit extends React.Component { 6 | componentWillMount() { 7 | Layout.configure({ 8 | defaultUnit: 'rem', 9 | }) 10 | } 11 | 12 | render() { 13 | return 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/configuration/CustomUnit.test.js: -------------------------------------------------------------------------------- 1 | it('Supports custom measurement unit', () => { 2 | cy.loadStory(['configuration', 'custom-configuration'], ['custom-unit']) 3 | 4 | cy.get('#notch').assertNotch() 5 | }) 6 | -------------------------------------------------------------------------------- /examples/hooks/UseBreakpointChange.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useBreakpointChange } from 'atomic-layout' 3 | 4 | const UseBreakpointChange = () => { 5 | const [text, setText] = useState('default') 6 | 7 | useBreakpointChange((breakpointName) => { 8 | setText(breakpointName) 9 | }) 10 | 11 | return ( 12 |
13 |

{text}

14 |
15 | ) 16 | } 17 | 18 | export default UseBreakpointChange 19 | -------------------------------------------------------------------------------- /examples/hooks/UseResponsiveProps.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'supported-styling-library' 3 | import { useResponsiveProps } from 'atomic-layout' 4 | 5 | const Avatar = styled.div` 6 | position: relative; 7 | background-color: #ccc; 8 | border-radius: 50%; 9 | height: ${({ size }) => size}px; 10 | width: ${({ size }) => size}px; 11 | ` 12 | 13 | const Badge = styled.span` 14 | display: inline-block; 15 | background-color: green; 16 | border-radius: 50%; 17 | border: 3px solid #fff; 18 | height: 10px; 19 | width: 10px; 20 | ` 21 | 22 | const Element = (props) => { 23 | const { size, showBadge, fontSize } = useResponsiveProps(props) 24 | 25 | return ( 26 |
27 | 28 | {showBadge && } 29 | 30 |
31 | ) 32 | } 33 | 34 | const UseResponsivePropsExample = () => ( 35 | 48 | ) 49 | 50 | export default UseResponsivePropsExample 51 | -------------------------------------------------------------------------------- /examples/hooks/UseResponsiveValue.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useResponsiveValue, Box } from 'atomic-layout' 3 | 4 | const UseResponsiveValue = () => { 5 | const text = useResponsiveValue( 6 | { 7 | xs: 'xs', 8 | md: 'md', 9 | lg: 'lg', 10 | }, 11 | 'default', 12 | ) 13 | 14 | return {text} 15 | } 16 | 17 | export default UseResponsiveValue 18 | -------------------------------------------------------------------------------- /examples/hooks/UseViewportChange.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import Layout, { useViewportChange, Box } from 'atomic-layout' 3 | 4 | const UseViewportChange = () => { 5 | const [isVisible, setVisibility] = useState(false) 6 | 7 | useViewportChange(() => { 8 | setVisibility( 9 | matchMedia(`(min-width: ${Layout.breakpoints.lg.minWidth})`).matches, 10 | ) 11 | }) 12 | 13 | return ( 14 | isVisible && ( 15 | 16 | Content 17 | 18 | ) 19 | ) 20 | } 21 | 22 | export default UseViewportChange 23 | -------------------------------------------------------------------------------- /examples/hooks/useBreakpointChange.test.js: -------------------------------------------------------------------------------- 1 | describe('useBreakpointChange', () => { 2 | before(() => { 3 | cy.loadStory(['hooks'], ['usebreakpointchange']) 4 | }) 5 | 6 | const assertText = (value) => { 7 | return cy.get('#element').should('have.text', value) 8 | } 9 | 10 | it('Renders with the correct initial state', () => { 11 | assertText('default') 12 | }) 13 | 14 | it('Sets breakpoint name on breakpoint change', () => { 15 | cy.setBreakpoint('sm').then(() => { 16 | assertText('sm') 17 | }) 18 | }) 19 | 20 | it('Ignores resize within the same breakpoint', () => { 21 | cy.viewport(580, 700).then(() => { 22 | assertText('sm') 23 | }) 24 | 25 | cy.viewport(600, 700).then(() => { 26 | assertText('sm') 27 | }) 28 | }) 29 | 30 | it('Re-renders on breakpoint change', () => { 31 | cy.setBreakpoint('md').then(() => { 32 | assertText('md') 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /examples/hooks/useResponsiveProps.test.js: -------------------------------------------------------------------------------- 1 | describe('useResponsiveProps', () => { 2 | before(() => { 3 | cy.loadStory(['hooks'], ['useresponsiveprops']) 4 | }) 5 | 6 | beforeEach(() => { 7 | cy.setBreakpoint('xs') 8 | }) 9 | 10 | const hasSize = (size) => { 11 | return cy 12 | .get('#avatar') 13 | .should('have.css', 'height', `${size}px`) 14 | .should('have.css', 'width', `${size}px`) 15 | } 16 | 17 | const hasFontSize = (size) => { 18 | return cy.get('#element').should('have.css', 'font-size', `${size}px`) 19 | } 20 | 21 | const hasBadge = (visible = true) => { 22 | return cy.get('#badge').should(visible ? 'be.visible' : 'not.be.visible') 23 | } 24 | 25 | it('High-pass', () => { 26 | hasSize(50) 27 | cy.setBreakpoint('sm').then(() => hasSize(50)) 28 | cy.setBreakpoint('md').then(() => hasSize(75)) 29 | cy.setBreakpoint('lg').then(() => hasSize(100)) 30 | cy.setBreakpoint('xl').then(() => hasSize(100)) 31 | }) 32 | 33 | it('Bell', () => { 34 | hasFontSize(16) 35 | cy.setBreakpoint('sm').then(() => hasFontSize(20)) 36 | cy.setBreakpoint('md').then(() => hasFontSize(20)) 37 | cy.setBreakpoint('lg').then(() => hasFontSize(16)) 38 | cy.setBreakpoint('xl').then(() => hasFontSize(16)) 39 | }) 40 | 41 | it('Notch', () => { 42 | hasBadge() 43 | cy.setBreakpoint('sm').then(() => hasBadge()) 44 | cy.setBreakpoint('md').then(() => hasBadge()) 45 | cy.setBreakpoint('lg').then(() => hasBadge(false)) 46 | cy.setBreakpoint('xl').then(() => hasBadge(false)) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /examples/hooks/useResponsiveValue.test.js: -------------------------------------------------------------------------------- 1 | describe('useResponsiveValue', () => { 2 | before(() => { 3 | cy.loadStory(['hooks'], ['useresponsivevalue']) 4 | }) 5 | 6 | const assertText = (value) => { 7 | return cy.get('#element').should('have.text', value) 8 | } 9 | 10 | it('Resolves to a value associated with a breakpoint', () => { 11 | assertText('xs') 12 | cy.setBreakpoint('md').then(() => assertText('md')) 13 | cy.setBreakpoint('lg').then(() => assertText('lg')) 14 | }) 15 | 16 | it('Fallbacks to the default value when no breakpoint association found', () => { 17 | cy.setBreakpoint('sm').then(() => assertText('default')) 18 | cy.setBreakpoint('xl').then(() => assertText('default')) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /examples/hooks/useViewportChange.test.js: -------------------------------------------------------------------------------- 1 | describe('useViewportChange', () => { 2 | before(() => { 3 | cy.loadStory(['hooks'], ['useviewportchange']) 4 | }) 5 | 6 | const element = () => cy.get('#element') 7 | 8 | it('Renders with the correct initial state', () => { 9 | element().should('not.be.visible') 10 | }) 11 | 12 | it('Does not render at unmatching viewport', () => { 13 | cy.setBreakpoint('sm').then(() => element().should('not.be.visible')) 14 | cy.setBreakpoint('md').then(() => element().should('not.be.visible')) 15 | }) 16 | 17 | it('Renders when the viewport matches', () => { 18 | cy.setBreakpoint('lg').then(() => element().should('be.visible')) 19 | cy.setBreakpoint('xl').then(() => element().should('be.visible')) 20 | }) 21 | 22 | it('Does not render when returned to non-matching viewport', () => { 23 | cy.setBreakpoint('sm').then(() => element().should('not.be.visible')) 24 | cy.setBreakpoint('md').then(() => element().should('not.be.visible')) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /examples/recipes/IterativeAreas.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const dataOne = [ 6 | { id: 'abc', text: 'Foo' }, 7 | { id: 'def', text: 'Bar' }, 8 | { id: 'ghi', text: 'Doe' }, 9 | ] 10 | 11 | const dataTwo = [ 12 | { id: 123, text: '123' }, 13 | { id: 456, text: '456' }, 14 | { id: 789, text: '789' }, 15 | ] 16 | 17 | const List = () => ( 18 | <> 19 |

Columns:

20 | 21 | {({ SingleArea }) => 22 | dataOne.map((entry) => ( 23 | 24 | {entry.text} 25 | 26 | )) 27 | } 28 | 29 | 30 |

Rows:

31 | 32 | {({ SingleArea }) => 33 | dataTwo.map((entry) => ( 34 | 35 | {entry.text} 36 | 37 | )) 38 | } 39 | 40 | 41 | ) 42 | 43 | export default List 44 | -------------------------------------------------------------------------------- /examples/recipes/IterativeAreas.test.js: -------------------------------------------------------------------------------- 1 | describe('Iterative areas', function() { 2 | before(() => { 3 | cy.loadStory(['recipes', 'all'], ['iterative-areas']) 4 | }) 5 | 6 | describe('Iterative columns', function() { 7 | it('Renders without crashing', () => { 8 | cy.get('#composition-one').should('be.visible') 9 | cy.get('#abc').should('be.visible') 10 | cy.get('#def').should('be.visible') 11 | cy.get('#ghi').should('be.visible') 12 | }) 13 | 14 | it('Respects col="auto" on dynamic area', () => { 15 | cy.get('#abc') 16 | .should('have.css', 'grid-column', 'auto / auto') 17 | .should('have.css', 'grid-row', 'singleArea / singleArea') 18 | .notIntersectWith('#def') 19 | .notIntersectWith('#ghi') 20 | 21 | cy.get('#def') 22 | .should('have.css', 'grid-column', 'auto / auto') 23 | .should('have.css', 'grid-row', 'singleArea / singleArea') 24 | .notIntersectWith('#abc') 25 | .notIntersectWith('#ghi') 26 | 27 | cy.get('#ghi') 28 | .should('have.css', 'grid-column', 'auto / auto') 29 | .should('have.css', 'grid-row', 'singleArea / singleArea') 30 | .notIntersectWith('#abc') 31 | .notIntersectWith('#def') 32 | }) 33 | }) 34 | 35 | describe('Iterative rows', function() { 36 | it('Renders without crashing', () => { 37 | cy.get('#composition-two').should('be.visible') 38 | cy.get('#123').should('be.visible') 39 | cy.get('#456').should('be.visible') 40 | cy.get('#789').should('be.visible') 41 | }) 42 | 43 | it('Respects row="auto" on dynamic area', () => { 44 | cy.get('#123') 45 | .should('have.css', 'grid-column', 'singleArea / singleArea') 46 | .should('have.css', 'grid-row', 'auto / auto') 47 | .notIntersectWith('#456') 48 | .notIntersectWith('#789') 49 | 50 | cy.get('#456') 51 | .should('have.css', 'grid-column', 'singleArea / singleArea') 52 | .should('have.css', 'grid-row', 'auto / auto') 53 | .notIntersectWith('#123') 54 | .notIntersectWith('#789') 55 | 56 | cy.get('#789') 57 | .should('have.css', 'grid-column', 'singleArea / singleArea') 58 | .should('have.css', 'grid-row', 'auto / auto') 59 | .notIntersectWith('#123') 60 | .notIntersectWith('#456') 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /examples/regression/DisplayNames.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box, Composition, Only, Visible } from 'atomic-layout' 3 | 4 | const DisplayNames = () => ( 5 |
6 | {Box.displayName} 7 | 12 | {(Areas) => ( 13 | {Areas.One.displayName} 14 | )} 15 | 16 | 17 | {Only.displayName} 18 | 19 | 20 | {Visible.displayName} 21 | 22 |
23 | ) 24 | 25 | export default DisplayNames 26 | -------------------------------------------------------------------------------- /examples/regression/DisplayNames.test.js: -------------------------------------------------------------------------------- 1 | describe('Display names', () => { 2 | before(() => { 3 | cy.loadStory(['regression', 'all'], ['display-names']) 4 | }) 5 | 6 | it('Box should have a meaningful display name', () => { 7 | cy.get('#box').should('have.text', 'Box') 8 | }) 9 | 10 | it('Composition should have meaningful display name', () => { 11 | cy.get('#composition').should( 12 | 'have.attr', 13 | 'data-display-name', 14 | 'Composition', 15 | ) 16 | }) 17 | 18 | it('Composition areas should have meaningful display name', () => { 19 | cy.get('#composition-area').should('have.text', 'Area(One)') 20 | }) 21 | 22 | it('Only should have meaningful display name', () => { 23 | cy.get('#only').should('have.text', 'Only') 24 | }) 25 | 26 | it('Visible should have meaningful display name', () => { 27 | cy.get('#visible').should('have.text', 'Visible') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /examples/regression/OnlyUnmount.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Only } from 'atomic-layout' 3 | 4 | const Child = ({ disabled }) => { 5 | const [count, setCount] = useState(0) 6 | 7 | return ( 8 |
9 |

10 | Count: {count} 11 |

12 | 19 |
20 | ) 21 | } 22 | 23 | const OnlyUnmount = () => { 24 | const [isDisabled, setDisabled] = useState(false) 25 | 26 | return ( 27 |
28 | 29 | 30 | 31 | 34 |
35 | ) 36 | } 37 | 38 | export default OnlyUnmount 39 | -------------------------------------------------------------------------------- /examples/regression/OnlyUnmount.test.js: -------------------------------------------------------------------------------- 1 | describe('Only unmount', () => { 2 | before(() => { 3 | cy.loadStory(['regression', 'all'], ['only-unmount']) 4 | }) 5 | 6 | describe('given I clicked on increment counter', () => { 7 | before(() => { 8 | cy.get('#button-increment') 9 | .click() 10 | .click() 11 | .click() 12 | }) 13 | 14 | it('should update the counter state', () => { 15 | cy.get('#count').should('have.text', '3') 16 | }) 17 | 18 | it('should retain state when parent updates', () => { 19 | cy.get('#button-disable').click() 20 | cy.get('#count').should('have.text', '3') 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /examples/regression/ParentRerendering.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Composition } from 'atomic-layout' 3 | 4 | const ParentRerendering = () => { 5 | const [value, setValue] = useState('') 6 | 7 | return ( 8 | 9 | {(Areas) => ( 10 | <> 11 | 12 |

Title

13 |
14 | 15 | setValue(event.target.value)} 19 | /> 20 | 21 | 22 | )} 23 |
24 | ) 25 | } 26 | 27 | export default ParentRerendering 28 | -------------------------------------------------------------------------------- /examples/regression/ParentRerendering.test.js: -------------------------------------------------------------------------------- 1 | describe('Parent re-rendering', () => { 2 | before(() => { 3 | cy.loadStory(['regression', 'all'], ['parent-re-rendering']) 4 | }) 5 | 6 | it('Should preserve areas state when parent updates', () => { 7 | cy.get('input[name="username"]') 8 | .type('admin') 9 | .should('be.focused') 10 | .should('have.value', 'admin') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /examples/regression/SingleResponsiveProp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | import Square from '@stories/Square' 4 | 5 | const SingleResponsiveProp = () => ( 6 | 12 | {(Areas) => ( 13 | <> 14 | 15 | Left 16 | 17 | 18 | Right 19 | 20 | 21 | )} 22 | 23 | ) 24 | 25 | export default SingleResponsiveProp 26 | -------------------------------------------------------------------------------- /examples/regression/SingleResponsiveProp.test.js: -------------------------------------------------------------------------------- 1 | describe('Single responsive prop', () => { 2 | before(() => { 3 | cy.loadStory(['regression', 'all'], ['single-responsive-prop']) 4 | }) 5 | 6 | describe('Given "up" behavior', () => { 7 | it('Renders no areas on unmatched breakpoints', () => { 8 | const shouldRenderNothing = () => { 9 | cy.get('[data-area="left"]').should('not.be.visible') 10 | cy.get('[data-area="right"]').should('not.be.visible') 11 | } 12 | 13 | cy.setBreakpoint('xs').then(shouldRenderNothing) 14 | cy.setBreakpoint('sm').then(shouldRenderNothing) 15 | }) 16 | 17 | it('Renders areas on matched breakpoint and up', () => { 18 | const shouldRenderAreas = () => { 19 | cy.get('#composition').assertAreas([['left', 'right']]) 20 | } 21 | 22 | cy.setBreakpoint('md').then(shouldRenderAreas) 23 | cy.setBreakpoint('lg').then(shouldRenderAreas) 24 | cy.setBreakpoint('xl').then(shouldRenderAreas) 25 | }) 26 | }) 27 | 28 | describe('Given "down" behavior', () => { 29 | it('Has no padding on unmatched breakpoints', () => { 30 | const shouldHaveNoPadding = () => { 31 | cy.get('#composition').should('have.css', 'padding', '0px') 32 | } 33 | 34 | cy.setBreakpoint('lg').then(shouldHaveNoPadding) 35 | cy.setBreakpoint('xl').then(shouldHaveNoPadding) 36 | }) 37 | 38 | it('Has padding on matched breakpoints', () => { 39 | const shouldHavePadding = () => { 40 | cy.get('#composition').should('have.css', 'padding', '15px') 41 | } 42 | 43 | cy.setBreakpoint('xs').then(shouldHavePadding) 44 | cy.setBreakpoint('sm').then(shouldHavePadding) 45 | cy.setBreakpoint('md').then(shouldHavePadding) 46 | }) 47 | }) 48 | 49 | describe('Given "only" behavior', () => { 50 | it('Has no gap on unmatched breakpoints', () => { 51 | const shouldHaveNoGap = () => { 52 | cy.get('#composition').should('have.css', 'grid-gap', 'normal normal') 53 | } 54 | 55 | cy.setBreakpoint('xs').then(shouldHaveNoGap) 56 | cy.setBreakpoint('sm').then(shouldHaveNoGap) 57 | cy.setBreakpoint('md').then(shouldHaveNoGap) 58 | }) 59 | 60 | it('Has a gap on a matched breakpoint', () => { 61 | const shouldHaveGap = () => { 62 | cy.get('#composition').should('have.css', 'grid-gap', '10px 10px') 63 | } 64 | 65 | cy.setBreakpoint('lg').then(shouldHaveGap) 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /examples/regression/StylesUndefined.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Composition } from 'atomic-layout' 3 | 4 | const TemplateIndentation = () => ( 5 | 6 | 7 | Must not contain gutter CSS property. 8 | 9 | 10 | ) 11 | 12 | export default TemplateIndentation 13 | -------------------------------------------------------------------------------- /examples/regression/StylesUndefined.test.js: -------------------------------------------------------------------------------- 1 | it('Omits style props with "undefined" value', () => { 2 | cy.loadStory(['regression', 'all'], ['styles-undefined']) 3 | 4 | cy.get('#composition') 5 | .should('be.visible') 6 | .should('have.css', 'padding-right', '12px') 7 | .should('not.have.css', 'gutter') 8 | }) 9 | -------------------------------------------------------------------------------- /examples/semantics/PolymorphicProp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'supported-styling-library' 3 | import { Composition } from 'atomic-layout' 4 | import Square from '@stories/Square' 5 | 6 | const CustomHeader = styled.header` 7 | background-color: #eee; 8 | display: block; 9 | padding: 10px; 10 | ` 11 | 12 | const PolymorphicProp = () => ( 13 | 14 | {({ Header, Main, Element, Footer }) => ( 15 | <> 16 | 19 |
20 | Main 21 |
22 | 23 | Default element 24 | 25 |
26 | Footer 27 |
28 | 29 | )} 30 |
31 | ) 32 | 33 | export default PolymorphicProp 34 | -------------------------------------------------------------------------------- /examples/semantics/PolymorphicProp.test.js: -------------------------------------------------------------------------------- 1 | describe('Polymorphic prop', () => { 2 | before(() => { 3 | cy.loadStory(['semantics'], ['polymorphic-prop']) 4 | }) 5 | 6 | it('Renders "div" by default', () => { 7 | cy.get('#default').haveTag('div') 8 | }) 9 | 10 | describe('When given a tag name string', () => { 11 | it('Renders the given tag', () => { 12 | cy.get('#main').haveTag('main') 13 | cy.get('#footer').haveTag('footer') 14 | }) 15 | }) 16 | 17 | describe('When given a custom React component', () => { 18 | it('Renders a custom React component', () => { 19 | cy.get('#header').haveTag('header') 20 | }) 21 | 22 | it('Preserves irrelevant styles', () => { 23 | cy.get('#header').should( 24 | 'have.css', 25 | 'background-color', 26 | 'rgb(238, 238, 238)', 27 | ) 28 | }) 29 | 30 | it('Overrides layout styles', () => { 31 | cy.get('#header') 32 | .should('have.css', 'display', 'block') 33 | .should('have.css', 'padding', '20px') 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /examples/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 3 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 4 | font-size: 14px; 5 | } 6 | -------------------------------------------------------------------------------- /examples/utils/MakeResponsive/MakeResponsive.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'supported-styling-library' 3 | import { makeResponsive } from 'atomic-layout' 4 | 5 | const Text = makeResponsive( 6 | styled.h1` 7 | color: ${({ color }) => color}; 8 | font-size: ${({ size }) => size}px; 9 | `, 10 | ) 11 | 12 | const Scenario = () => { 13 | const textRef = React.useRef({}) 14 | const [state, setState] = React.useState('red') 15 | const [refDerivedData, setRefDerivedData] = React.useState(null) 16 | 17 | React.useEffect(() => { 18 | setRefDerivedData(textRef.current.id) 19 | }, [textRef.current]) 20 | 21 | return ( 22 |
23 | 33 |

34 | Quick brown fox (#{refDerivedData}) 35 |

36 | 39 |
40 |
41 | ) 42 | } 43 | 44 | export default Scenario 45 | -------------------------------------------------------------------------------- /examples/utils/MakeResponsive/makeResponsive.test.js: -------------------------------------------------------------------------------- 1 | describe('makeResponsive', () => { 2 | before(() => { 3 | cy.loadStory(['utilities'], ['makeresponsive']) 4 | }) 5 | 6 | describe('Prmiary behavior', () => { 7 | it('Custom responsive prop has correct initial state', () => { 8 | cy.get('#text') 9 | .should('have.css', 'font-size', '16px') 10 | .should('have.css', 'color', 'rgb(0, 0, 0)') 11 | }) 12 | 13 | it('Custom responsive prop changes correctly on breakpoint changes', () => { 14 | cy.setBreakpoint('sm').then(() => { 15 | cy.get('#text') 16 | .should('have.css', 'font-size', '16px') 17 | .should('have.css', 'color', 'rgb(0, 128, 0)') 18 | }) 19 | 20 | cy.setBreakpoint('md').then(() => { 21 | cy.get('#text') 22 | .should('have.css', 'font-size', '20px') 23 | .should('have.css', 'color', 'rgb(0, 128, 0)') 24 | }) 25 | 26 | cy.setBreakpoint('lg').then(() => { 27 | cy.get('#text') 28 | .should('have.css', 'font-size', '24px') 29 | .should('have.css', 'color', 'rgb(255, 0, 0)') 30 | }) 31 | 32 | cy.setBreakpoint('xl').then(() => { 33 | cy.get('#text') 34 | .should('have.css', 'font-size', '24px') 35 | .should('have.css', 'color', 'rgb(255, 0, 0)') 36 | }) 37 | }) 38 | }) 39 | 40 | describe('Generic behavior', () => { 41 | it('Supports ref forwarding', () => { 42 | cy.get('#ref').should('have.text', '#text') 43 | }) 44 | 45 | it('Re-renders on props change', () => { 46 | cy.setBreakpoint('lg') 47 | cy.get('#change-color').click() 48 | cy.get('#text').should('have.css', 'color', 'rgb(238, 130, 238)') 49 | 50 | // Test that the updated value persists after breakpoint change. 51 | // This ensures that "useResponsiveProps" handles both direct 52 | // and responsive props updates. 53 | cy.setBreakpoint('md') 54 | cy.setBreakpoint('lg') 55 | cy.get('#text').should('have.css', 'color', 'rgb(238, 130, 238)') 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /examples/utils/Query/Query.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'supported-styling-library' 3 | import { query } from 'atomic-layout' 4 | 5 | const Component = styled.div` 6 | font-size: 14px; 7 | 8 | /* Single breakpoint */ 9 | @media ${query({ for: 'sm' })} { 10 | color: cyan; 11 | } 12 | 13 | /* High-pass */ 14 | @media ${query({ from: 'md' })} { 15 | background-color: lightcyan; 16 | } 17 | 18 | /* Low-pass */ 19 | @media ${query({ to: 'sm' })} { 20 | padding: 10px; 21 | } 22 | 23 | /* Bell */ 24 | @media ${query({ from: 'md', to: 'xl' })} { 25 | margin: 20px; 26 | } 27 | 28 | /* Notch */ 29 | @media ${query({ except: true, from: 'md', to: 'lg' })} { 30 | font-size: 18px; 31 | } 32 | ` 33 | 34 | const QueryExample = () => { 35 | return Content 36 | } 37 | 38 | export default QueryExample 39 | -------------------------------------------------------------------------------- /examples/utils/Query/query.test.js: -------------------------------------------------------------------------------- 1 | describe('query()', () => { 2 | before(() => { 3 | cy.loadStory(['utilities'], ['query']) 4 | }) 5 | 6 | const assertCss = (propertyName, value) => { 7 | return cy 8 | .get('[data-test-id="component"]') 9 | .should('have.css', propertyName, value) 10 | } 11 | 12 | it('Supports an exact breakpoint (for)', () => { 13 | cy.setBreakpoint('xs').then(() => assertCss('color', 'rgb(0, 0, 0)')) 14 | cy.setBreakpoint('sm').then(() => assertCss('color', 'rgb(0, 255, 255)')) 15 | cy.setBreakpoint('md').then(() => assertCss('color', 'rgb(0, 0, 0)')) 16 | cy.setBreakpoint('lg').then(() => assertCss('color', 'rgb(0, 0, 0)')) 17 | cy.setBreakpoint('xl').then(() => assertCss('color', 'rgb(0, 0, 0)')) 18 | }) 19 | 20 | it('Supports a high-pass breakpoint range (from)', () => { 21 | cy.setBreakpoint('xs').then(() => 22 | assertCss('background-color', 'rgba(0, 0, 0, 0)'), 23 | ) 24 | cy.setBreakpoint('sm').then(() => 25 | assertCss('background-color', 'rgba(0, 0, 0, 0)'), 26 | ) 27 | cy.setBreakpoint('md').then(() => 28 | assertCss('background-color', 'rgb(224, 255, 255)'), 29 | ) 30 | cy.setBreakpoint('lg').then(() => 31 | assertCss('background-color', 'rgb(224, 255, 255)'), 32 | ) 33 | cy.setBreakpoint('xl').then(() => 34 | assertCss('background-color', 'rgb(224, 255, 255)'), 35 | ) 36 | }) 37 | 38 | it('Supports a low-pass breakpoint range (to)', () => { 39 | cy.setBreakpoint('xs').then(() => assertCss('padding', '10px')) 40 | cy.setBreakpoint('sm').then(() => assertCss('padding', '0px')) 41 | cy.setBreakpoint('md').then(() => assertCss('padding', '0px')) 42 | cy.setBreakpoint('lg').then(() => assertCss('padding', '0px')) 43 | cy.setBreakpoint('xl').then(() => assertCss('padding', '0px')) 44 | }) 45 | 46 | it('Supports a bell breakpoint range (from/to)', () => { 47 | cy.setBreakpoint('xs').then(() => assertCss('margin', '0px')) 48 | cy.setBreakpoint('sm').then(() => assertCss('margin', '0px')) 49 | cy.setBreakpoint('md').then(() => assertCss('margin', '20px')) 50 | cy.setBreakpoint('lg').then(() => assertCss('margin', '20px')) 51 | cy.setBreakpoint('xl').then(() => assertCss('margin', '0px')) 52 | }) 53 | 54 | it('Supports a notch breakpoint range (except/from/to)', () => { 55 | cy.setBreakpoint('xs').then(() => assertCss('font-size', '18px')) 56 | cy.setBreakpoint('sm').then(() => assertCss('font-size', '18px')) 57 | cy.setBreakpoint('md').then(() => assertCss('font-size', '14px')) 58 | cy.setBreakpoint('lg').then(() => assertCss('font-size', '18px')) 59 | cy.setBreakpoint('xl').then(() => assertCss('font-size', '18px')) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "independent", 4 | "npmClient": "yarn", 5 | "useWorkspaces": true 6 | } 7 | -------------------------------------------------------------------------------- /logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/logo-full.png -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /materials/example-thumbnails/basic-composition.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/basic-composition.jpg -------------------------------------------------------------------------------- /materials/example-thumbnails/codesandbox/thumbnail-blue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/codesandbox/thumbnail-blue.jpg -------------------------------------------------------------------------------- /materials/example-thumbnails/codesandbox/thumbnail-violet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/codesandbox/thumbnail-violet.jpg -------------------------------------------------------------------------------- /materials/example-thumbnails/codesandbox/thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/codesandbox/thumbnail.jpg -------------------------------------------------------------------------------- /materials/example-thumbnails/conditional-rendering.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/conditional-rendering.jpg -------------------------------------------------------------------------------- /materials/example-thumbnails/custom-configuration.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/custom-configuration.jpg -------------------------------------------------------------------------------- /materials/example-thumbnails/nested-composition.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/nested-composition.jpg -------------------------------------------------------------------------------- /materials/example-thumbnails/responsive-props.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/responsive-props.jpg -------------------------------------------------------------------------------- /materials/example-thumbnails/shorthand-media-query.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/example-thumbnails/shorthand-media-query.jpg -------------------------------------------------------------------------------- /materials/github/contrib-e2e-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/github/contrib-e2e-test.png -------------------------------------------------------------------------------- /materials/github/contrib-new-story.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/github/contrib-new-story.png -------------------------------------------------------------------------------- /materials/github/contrib-unit-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/github/contrib-unit-test.png -------------------------------------------------------------------------------- /materials/github/gitub-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/github/gitub-preview.jpg -------------------------------------------------------------------------------- /materials/react-finland-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/react-finland-thumbnail.jpg -------------------------------------------------------------------------------- /materials/react-vienna-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/materials/react-vienna-thumbnail.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atomic-layout", 3 | "description": "Physical representation of layout composition to create declarative, responsive layouts in React.", 4 | "license": "MIT", 5 | "private": true, 6 | "workspaces": { 7 | "packages": [ 8 | "packages/*" 9 | ] 10 | }, 11 | "scripts": { 12 | "lint": "lerna exec yarn lint", 13 | "clean": "lerna exec yarn clean", 14 | "build": "lerna exec yarn build", 15 | "test": "lerna exec yarn test", 16 | "verify": "lerna exec yarn verify" 17 | }, 18 | "husky": { 19 | "hooks": { 20 | "pre-commit": "lint-staged" 21 | } 22 | }, 23 | "lint-staged": { 24 | "*.{ts,tsx}": [ 25 | "yarn lint", 26 | "prettier --write", 27 | "git add" 28 | ] 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "7.7.2", 32 | "@babel/plugin-proposal-class-properties": "7.7.0", 33 | "@babel/plugin-proposal-export-default-from": "7.5.2", 34 | "@babel/plugin-proposal-export-namespace-from": "7.5.2", 35 | "@babel/plugin-proposal-object-rest-spread": "7.6.2", 36 | "@babel/preset-env": "7.7.4", 37 | "@babel/preset-react": "7.7.4", 38 | "@babel/preset-typescript": "7.7.4", 39 | "@storybook/addon-actions": "5.3.7", 40 | "@storybook/addon-links": "5.3.7", 41 | "@storybook/react": "5.3.7", 42 | "chalk": "3.0.0", 43 | "cross-env": "5.2.0", 44 | "husky": "3.1.0", 45 | "jest": "24.9.0", 46 | "lerna": "3.16.4", 47 | "lint-staged": "9.5.0", 48 | "prettier": "1.19.1", 49 | "rimraf": "3.0.0", 50 | "rollup": "1.27.5", 51 | "rollup-plugin-node-resolve": "5.2.0", 52 | "rollup-plugin-replace": "2.2.0", 53 | "rollup-plugin-typescript": "1.0.1", 54 | "storybook": "5.3.7", 55 | "ts-jest": "24.2.0", 56 | "tslib": "1.10.0", 57 | "tslint": "5.19.0", 58 | "tslint-config-prettier": "1.18.0", 59 | "typescript": "3.7.2" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Artem Zakharchenko 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. -------------------------------------------------------------------------------- /packages/atomic-layout-core/global.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Global flag that indicates production environment. 3 | */ 4 | declare const __PROD__: string 5 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Atomic Layout core', 3 | verbose: true, 4 | roots: ['src'], 5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], 6 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 7 | transform: { 8 | '^.+\\.tsx?$': 'ts-jest', 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/jest.setup.js: -------------------------------------------------------------------------------- 1 | // Perform all unit tests in production environment 2 | global.__PROD__ = true 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@atomic-layout/core", 3 | "description": "Atomic Layout core module", 4 | "version": "0.14.1", 5 | "license": "MIT", 6 | "main": "lib/index.js", 7 | "typings": "lib/index.d.ts", 8 | "scripts": { 9 | "start": "rollup -cw", 10 | "lint": "tslint 'src/**/*.ts'", 11 | "test": "jest --runInBand", 12 | "clean": "rimraf lib", 13 | "build:types": "ttsc --skipLibCheck", 14 | "build": "cross-env NODE_ENV=production rollup -c", 15 | "verify": "yarn lint && yarn test && yarn build", 16 | "prepublishOnly": "yarn verify" 17 | }, 18 | "devDependencies": { 19 | "@types/jest": "24.0.23", 20 | "@types/match-media-mock": "^0.1.5", 21 | "@types/node": "12.12.14", 22 | "cross-env": "5.2.0", 23 | "jest": "24.9.0", 24 | "match-media-mock": "0.1.1", 25 | "react": "16.12.0", 26 | "typescript": "3.7.2" 27 | }, 28 | "files": [ 29 | "lib" 30 | ], 31 | "publishConfig": { 32 | "access": "public" 33 | }, 34 | "author": { 35 | "name": "Artem Zakharchenko", 36 | "email": "kettanaito@gmail.com" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/kettanaito/atomic-layout/tree/master/packages/atomic-layout-core" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/kettanaito/atomic-layout/issues" 44 | }, 45 | "funding": { 46 | "type": "opencollective", 47 | "url": "https://opencollective.com/atomic-layout" 48 | }, 49 | "keywords": [ 50 | "atomic", 51 | "layout", 52 | "core" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import replace from 'rollup-plugin-replace' 3 | import resolve from 'rollup-plugin-node-resolve' 4 | import commonjs from 'rollup-plugin-commonjs' 5 | import typescript from 'rollup-plugin-typescript2' 6 | import packageJson from './package.json' 7 | 8 | const { NODE_ENV: nodeEnv } = process.env 9 | const PRODUCTION = nodeEnv === 'production' 10 | const __PROD__ = PRODUCTION ? 'true' : '""' 11 | 12 | export default { 13 | input: 'src/index.ts', 14 | output: { 15 | file: path.resolve(__dirname, packageJson.main), 16 | format: 'cjs', 17 | exports: 'named', 18 | }, 19 | plugins: [ 20 | resolve({ 21 | mainFields: ['esnext'], 22 | extensions: ['.ts'], 23 | }), 24 | typescript(), 25 | commonjs(), 26 | replace({ 27 | __PROD__, 28 | }), 29 | ], 30 | } 31 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/Layout.ts: -------------------------------------------------------------------------------- 1 | import defaultOptions, { 2 | LayoutOptions, 3 | Breakpoints, 4 | MeasurementUnit, 5 | BreakpointBehavior, 6 | } from './const/defaultOptions' 7 | import invariant from './utils/functions/invariant' 8 | import warn from './utils/functions/warn' 9 | 10 | class Layout { 11 | public defaultUnit: MeasurementUnit = defaultOptions.defaultUnit 12 | public defaultBehavior: BreakpointBehavior = defaultOptions.defaultBehavior 13 | public breakpoints: Breakpoints = defaultOptions.breakpoints 14 | public defaultBreakpointName: string = defaultOptions.defaultBreakpointName 15 | protected isConfigureCalled: boolean = false 16 | 17 | constructor(options?: Partial) { 18 | return options ? this.configure(options, false) : this 19 | } 20 | 21 | /** 22 | * Applies global layout options. 23 | * Make sure to call this method once, preferably on the rool level 24 | * of your application. 25 | */ 26 | public configure(options: Partial, warnOnMultiple = true) { 27 | if (warnOnMultiple) { 28 | warn( 29 | !this.isConfigureCalled, 30 | 'Failed to configure Layout: do not call `Layout.configure()` more than once. Layout configuration must remain consistent throughout the application.', 31 | ) 32 | } 33 | 34 | invariant( 35 | options && typeof options === 'object', 36 | `Failed to configure Layout: expected an options Object, but got: ${options}.`, 37 | ) 38 | 39 | Object.keys(options || {}).forEach((optionName: keyof LayoutOptions) => { 40 | ;(this[optionName] as any) = options[optionName] 41 | }) 42 | 43 | invariant( 44 | this.breakpoints, 45 | 'Failed to configure Layout: expected to have at least one breakpoint specified, but got none.', 46 | ) 47 | 48 | invariant( 49 | this.breakpoints.hasOwnProperty(this.defaultBreakpointName), 50 | `Failed to configure Layout: cannot use "${this.defaultBreakpointName}" as the default breakpoint (breakpoint not found).`, 51 | ) 52 | 53 | invariant( 54 | this.defaultBreakpointName, 55 | `Failed to configure Layout: expected "defaultBreakpointName" property set, but got: ${this.defaultBreakpointName}.`, 56 | ) 57 | 58 | // Mark configure method as called to prevent its multiple calls 59 | this.isConfigureCalled = warnOnMultiple 60 | 61 | return this 62 | } 63 | } 64 | 65 | export default new Layout() 66 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/const/defaultOptions.ts: -------------------------------------------------------------------------------- 1 | type AbsoluteUnits = 'cm' | 'mm' | 'in' | 'px' | 'pt' | 'pc' 2 | type RelativeUnits = 3 | | '%' 4 | | 'em' 5 | | 'ex' 6 | | 'ch' 7 | | 'rem' 8 | | 'vw' 9 | | 'vh' 10 | | 'vmin' 11 | | 'vmax' 12 | 13 | export type Numeric = number | string 14 | export type MeasurementUnit = AbsoluteUnits | RelativeUnits 15 | export type BreakpointBehavior = 'up' | 'down' | 'only' 16 | export interface Breakpoints { 17 | [breakpointName: string]: Breakpoint 18 | } 19 | export interface LayoutOptions { 20 | /** 21 | * Measurement unit that suffixes numeric prop values. 22 | * @default "px" 23 | * @example 24 | * 25 | * // "padding: 10px" 26 | */ 27 | defaultUnit: MeasurementUnit 28 | /** 29 | * Map of layout breakpoints. 30 | */ 31 | breakpoints: Breakpoints 32 | /** 33 | * Breakpoint name to use when no explicit breakpoint 34 | * name is specified in a prop name. 35 | * @default "xs" 36 | */ 37 | defaultBreakpointName: string 38 | defaultBehavior: BreakpointBehavior 39 | } 40 | 41 | export interface MediaQuery { 42 | minHeight?: Numeric 43 | maxHeight?: Numeric 44 | minWidth?: Numeric 45 | maxWidth?: Numeric 46 | minResolution?: string 47 | maxResolution?: string 48 | aspectRatio?: string 49 | minAspectRatio?: string 50 | maxAspectRatio?: string 51 | scan?: 'interlace' | 'progressive' 52 | orientation?: 'portrait' | 'landscape' 53 | displayMode?: 'fullscreen' | 'standalone' | 'minimal-ui' | 'browser' 54 | } 55 | 56 | export interface Breakpoint extends MediaQuery { 57 | /* Index signature for dynamic breakpoint composition */ 58 | [propName: string]: any 59 | } 60 | 61 | const defaultOptions: LayoutOptions = { 62 | defaultUnit: 'px', 63 | defaultBehavior: 'up', 64 | defaultBreakpointName: 'xs', 65 | breakpoints: { 66 | xs: { 67 | maxWidth: '575px', 68 | }, 69 | sm: { 70 | minWidth: '576px', 71 | maxWidth: '767px', 72 | }, 73 | md: { 74 | minWidth: '768px', 75 | maxWidth: '991px', 76 | }, 77 | lg: { 78 | minWidth: '992px', 79 | maxWidth: '1199px', 80 | }, 81 | xl: { 82 | minWidth: '1200px', 83 | }, 84 | }, 85 | } 86 | 87 | export default defaultOptions 88 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Layout } from './Layout' 2 | export { default as defaultOptions } from './const/defaultOptions' 3 | 4 | /* Typings */ 5 | export { 6 | Numeric, 7 | MediaQuery, 8 | Breakpoint, 9 | Breakpoints, 10 | BreakpointBehavior, 11 | } from './const/defaultOptions' 12 | export { 13 | GenericProps, 14 | GridProps, 15 | BoxProps, 16 | CompositionProps, 17 | CompositionRenderProp, 18 | } from './const/props' 19 | 20 | /* Styles */ 21 | export { default as applyStyles } from './utils/styles/applyStyles' 22 | export { 23 | default as createMediaQuery, 24 | joinQueryList, 25 | } from './utils/styles/createMediaQuery' 26 | export { default as normalizeQuery } from './utils/styles/normalizeQuery' 27 | 28 | /* Breakpoints */ 29 | export { default as withBreakpoints } from './utils/breakpoints/withBreakpoints' 30 | export { default as openBreakpoint } from './utils/breakpoints/openBreakpoint' 31 | export { default as closeBreakpoint } from './utils/breakpoints/closeBreakpoint' 32 | export { default as mergeAreaRecords } from './utils/breakpoints/mergeAreaRecords' 33 | 34 | /* Props */ 35 | export { 36 | default as propAliases, 37 | PropAliases, 38 | PropAliasDeclaration, 39 | } from './const/propAliases' 40 | export { 41 | default as parsePropName, 42 | ParsedProp, 43 | ParsedBreakpoint, 44 | } from './utils/strings/parsePropName' 45 | export { default as parseTemplates } from './utils/templates/parseTemplates' 46 | export { 47 | default as generateComponents, 48 | AreasMap, 49 | AreaComponent, 50 | } from './utils/templates/generateComponents' 51 | 52 | /* Utilities */ 53 | export { default as warn } from './utils/functions/warn' 54 | export { default as compose } from './utils/functions/compose' 55 | export { default as throttle } from './utils/functions/throttle' 56 | export { default as transformNumeric } from './utils/math/transformNumeric' 57 | export { default as memoizeWith } from './utils/functions/memoizeWith' 58 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/closeBreakpoint/closeBreakpoint.spec.ts: -------------------------------------------------------------------------------- 1 | import closeBreakpoint from './closeBreakpoint' 2 | 3 | describe('closeBreakpoint', () => { 4 | describe('given numeric breakpoints', () => { 5 | const breakpoint = closeBreakpoint({ 6 | minHeight: 350, 7 | maxHeight: 700, 8 | minWidth: 500, 9 | maxWidth: 621, 10 | }) 11 | 12 | it('should not contain original "min-" properties', () => { 13 | expect(breakpoint).not.toContain(['minHeight', 'minWidth']) 14 | }) 15 | 16 | it('should coerce "max-" properties into {min} - 1', () => { 17 | expect(breakpoint).toHaveProperty('maxHeight', 349) 18 | expect(breakpoint).toHaveProperty('maxWidth', 499) 19 | }) 20 | }) 21 | 22 | describe('given custom measurement unit', () => { 23 | const breakpoint = closeBreakpoint({ 24 | minHeight: '350px', 25 | maxHeight: '700px', 26 | minWidth: '500px', 27 | maxWidth: '621px', 28 | }) 29 | 30 | it('should not contain original "min-" properties', () => { 31 | expect(breakpoint).not.toContain(['minHeight', 'minWidth']) 32 | }) 33 | 34 | it('should coerce "max-" properties into {min} - 1 + unit', () => { 35 | expect(breakpoint).toHaveProperty('maxHeight', '349px') 36 | expect(breakpoint).toHaveProperty('maxWidth', '499px') 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/closeBreakpoint/closeBreakpoint.ts: -------------------------------------------------------------------------------- 1 | import { Breakpoint } from '../../../const/defaultOptions' 2 | import getPrefix from '../../strings/getPrefix' 3 | 4 | /** 5 | * Accepts a breakpoint and returns a new breakpoint where 6 | * all the "min" properties of the original breakpoint are 7 | * flipped into the "max" properties. Any "max" properties 8 | * of the original breakpoint are omitted. 9 | * Subtracts 1 from the numeric values of all "min" properties 10 | * to not overlap with the given breakpoint. 11 | * 12 | * @example 13 | * flipBreakpoint({ minWidth: 500, maxWidth: 600 }) 14 | * // { maxWidth: 499 } 15 | */ 16 | export default function flipBreakpoint(breakpoint: Breakpoint): Breakpoint { 17 | return Object.entries(breakpoint) 18 | .map<[string, string, any]>(([propName, propValue]) => [ 19 | getPrefix(propName), 20 | propName, 21 | propValue, 22 | ]) 23 | .filter(([prefix]) => prefix !== 'max') 24 | .reduce((newBreakpoint, [prefix, propName, propValue]) => { 25 | const hasMinPrefix = prefix === 'min' 26 | const nextPropName = hasMinPrefix 27 | ? propName.replace(/^min/, 'max') 28 | : propName 29 | 30 | // Parse a breakpoint property value into a numeric value 31 | // and its measurement unit. 32 | const [, numericValue, unit] = /(\d+)(.+)?/.exec(propValue) 33 | 34 | /** 35 | * Subtracts 1 from the edge to not include the area at the beginning 36 | * of the breakpoint. 37 | * 38 | * @todo 39 | * How is "parseFloat" going to work with non-dimensional options? 40 | * (i.e. aspectRatio) 41 | */ 42 | const nextNumericValue = hasMinPrefix 43 | ? parseFloat(numericValue) - 1 44 | : numericValue 45 | 46 | // Append back the measurement unit. 47 | // Prevents breakpoints like { "minWidth": "768px" } 48 | // becoming { "minWidth": 768 } -> { "minWidth": "768rem" } 49 | // when the measurement unit is not "px". 50 | const nextValue = unit ? `${nextNumericValue}${unit}` : nextNumericValue 51 | 52 | return { 53 | ...newBreakpoint, 54 | [nextPropName]: nextValue, 55 | } 56 | }, {}) 57 | } 58 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/closeBreakpoint/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './closeBreakpoint' 2 | export * from './closeBreakpoint' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/getAreaRecords/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './getAreaRecords' 2 | export * from './getAreaRecords' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/mergeAreaRecords/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './mergeAreaRecords' 2 | export * from './mergeAreaRecords' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/mergeAreaRecords/mergeAreaRecords.ts: -------------------------------------------------------------------------------- 1 | import { AreaRecord } from '../getAreaRecords' 2 | import { Breakpoint } from '../../../const/defaultOptions' 3 | import transformNumeric from '../../math/transformNumeric' 4 | import getPrefix from '../../strings/getPrefix' 5 | 6 | /** 7 | * Merges two given area records. 8 | */ 9 | export default function mergeAreaRecords( 10 | nextAreaRecord: AreaRecord, 11 | prevAreaRecord: AreaRecord, 12 | includesArea: boolean, 13 | ): AreaRecord { 14 | const { behavior: prevRecordBehavior } = prevAreaRecord 15 | const { behavior: nextRecordBehavior } = nextAreaRecord 16 | 17 | const wentUp = prevRecordBehavior === 'up' 18 | const goesDown = nextRecordBehavior === 'down' 19 | const behavesSame = prevRecordBehavior === nextRecordBehavior 20 | const behavesInclusive = wentUp && goesDown 21 | const shouldStretch = wentUp 22 | 23 | const nextBehavior = 24 | !includesArea && shouldStretch ? 'down' : nextRecordBehavior 25 | 26 | const mergedBreakpoint = { 27 | ...prevAreaRecord.breakpoint, 28 | ...nextAreaRecord.breakpoint, 29 | } 30 | 31 | const nextBreakpoint = Object.keys(mergedBreakpoint).reduce( 32 | (acc, propName) => { 33 | let nextValue = mergedBreakpoint[propName] 34 | const prefix = getPrefix(propName) 35 | 36 | if (prefix === 'max') { 37 | if (!includesArea && shouldStretch) { 38 | const reversedValue = 39 | nextAreaRecord.breakpoint[propName.replace(/^max/, 'min')] 40 | nextValue = `calc(${transformNumeric(reversedValue)} - 1px)` 41 | } 42 | } 43 | 44 | if (prefix === 'min') { 45 | if (includesArea) { 46 | if (behavesSame || behavesInclusive) { 47 | nextValue = prevAreaRecord.breakpoint[propName] 48 | } 49 | } else { 50 | if (shouldStretch) { 51 | nextValue = prevAreaRecord.breakpoint[propName] 52 | } 53 | } 54 | } 55 | 56 | return { 57 | ...acc, 58 | [propName]: nextValue, 59 | } 60 | }, 61 | {}, 62 | ) 63 | 64 | return { 65 | behavior: nextBehavior, 66 | breakpoint: nextBreakpoint, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/openBreakpoint/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './openBreakpoint' 2 | export * from './openBreakpoint' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/openBreakpoint/openBreakpoint.spec.ts: -------------------------------------------------------------------------------- 1 | import { Breakpoint } from '../../../const/defaultOptions' 2 | import openBreakpoint from './openBreakpoint' 3 | 4 | describe('openBreakpoint', () => { 5 | describe('given already open breakpoint', () => { 6 | const breakpoint = { 7 | minHeight: 500, 8 | } 9 | 10 | it('should return breakpoint as-is', () => { 11 | expect(openBreakpoint(breakpoint)).toEqual({ 12 | minHeight: 500, 13 | }) 14 | }) 15 | }) 16 | 17 | describe('given a closed breakpoint', () => { 18 | const breakpoint = { 19 | maxWidth: 768, 20 | } 21 | 22 | it('should return an empty breakpoint', () => { 23 | expect(openBreakpoint(breakpoint)).toEqual({}) 24 | }) 25 | }) 26 | 27 | describe('given an inclusive breakpoint', () => { 28 | let result: Breakpoint 29 | const breakpoint = { 30 | minResolution: '150dpi', 31 | maxResolution: '300dpi', 32 | } 33 | 34 | beforeAll(() => { 35 | result = openBreakpoint(breakpoint) 36 | }) 37 | 38 | it('should set "maxResolution" to undefined', () => { 39 | expect(result).toHaveProperty('maxResolution', undefined) 40 | }) 41 | 42 | it('should preserve "minResolution" value', () => { 43 | expect(result).toHaveProperty('minResolution', '150dpi') 44 | }) 45 | }) 46 | 47 | describe('given a complex breakpoint', () => { 48 | let result: Breakpoint 49 | const breakpoint = { 50 | minWidth: 300, 51 | maxWidth: 500, 52 | minHeight: 200, 53 | maxHeight: 400, 54 | minResolution: '150dpi', 55 | maxResolution: '300dpi', 56 | maxAspectRatio: '1/3', 57 | } 58 | 59 | beforeAll(() => { 60 | result = openBreakpoint(breakpoint) 61 | }) 62 | 63 | it('should set all "max-" properties to undefined', () => { 64 | expect(result).toHaveProperty('maxWidth', undefined) 65 | expect(result).toHaveProperty('maxHeight', undefined) 66 | expect(result).toHaveProperty('maxResolution', undefined) 67 | expect(result).toHaveProperty('maxAspectRatio', undefined) 68 | }) 69 | 70 | it('should preserve all "min-" properties', () => { 71 | expect(result).toHaveProperty('minWidth', 300) 72 | expect(result).toHaveProperty('minHeight', 200) 73 | expect(result).toHaveProperty('minResolution', '150dpi') 74 | }) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/openBreakpoint/openBreakpoint.ts: -------------------------------------------------------------------------------- 1 | import { Breakpoint } from '../../../const/defaultOptions' 2 | import getPrefix from '../../strings/getPrefix' 3 | 4 | /** 5 | * Opens the given breakpoint. 6 | * A breakpoint is considered open when it has no upper boundary. For example, 7 | * a breakpoint that has "maxWidth: undefined" is the open breakpoint. 8 | */ 9 | export default function openBreakpoint( 10 | breakpoint: Breakpoint, 11 | ): T { 12 | return Object.keys(breakpoint).reduce( 13 | (acc, key) => ({ 14 | ...acc, 15 | [key]: getPrefix(key) === 'max' ? undefined : breakpoint[key], 16 | }), 17 | {} as T, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/shouldMergeBreakpoints/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './shouldMergeBreakpoints' 2 | export * from './shouldMergeBreakpoints' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/shouldMergeBreakpoints/shouldMergeBreakpoints.spec.ts: -------------------------------------------------------------------------------- 1 | import shouldMergeBreakpoints from './shouldMergeBreakpoints' 2 | 3 | describe('shouldMergeBreakpoints', () => { 4 | it('returns "true" for combinable breakpoints', () => { 5 | expect( 6 | shouldMergeBreakpoints( 7 | { 8 | maxWidth: 576, 9 | }, 10 | { 11 | minWidth: 580, 12 | maxWidth: 750, 13 | }, 14 | ), 15 | ).toBe(true) 16 | 17 | expect( 18 | shouldMergeBreakpoints( 19 | { 20 | minResolution: '70dpi', 21 | }, 22 | { 23 | minResolution: '320dpi', 24 | }, 25 | ), 26 | ).toBe(true) 27 | 28 | expect( 29 | shouldMergeBreakpoints( 30 | { 31 | height: 200, 32 | }, 33 | { 34 | height: 500, 35 | }, 36 | ), 37 | ).toBe(true) 38 | 39 | expect( 40 | shouldMergeBreakpoints( 41 | { 42 | minAspectRatio: '3/4', 43 | }, 44 | { 45 | minAspectRatio: '3/4', 46 | maxAspectRatio: '16/9', 47 | }, 48 | ), 49 | ).toBe(true) 50 | }) 51 | 52 | it('returns "false" for non-combinable breakpoints', () => { 53 | expect( 54 | shouldMergeBreakpoints( 55 | { 56 | maxWidth: 500, 57 | }, 58 | { 59 | width: 600, 60 | }, 61 | ), 62 | ).toBe(false) 63 | 64 | expect( 65 | shouldMergeBreakpoints( 66 | { 67 | resolution: '300dpi', 68 | }, 69 | { 70 | maxResolution: '300dpi', 71 | }, 72 | ), 73 | ).toBe(false) 74 | }) 75 | 76 | it('accounts property names with equal length', () => { 77 | expect( 78 | shouldMergeBreakpoints( 79 | { 80 | minFooBar: 1, 81 | }, 82 | { 83 | minFooBar: 2, 84 | maxDoeBar: 3, 85 | }, 86 | ), 87 | ).toBe(false) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/shouldMergeBreakpoints/shouldMergeBreakpoints.ts: -------------------------------------------------------------------------------- 1 | import { Breakpoint } from '../../../const/defaultOptions' 2 | 3 | /** 4 | * Replaces the prefixes in a parameter name. 5 | * Allows strict comparison of same parameters with different prefixes. 6 | * Does not test for inclusion/notch. 7 | * 8 | * @example 9 | * neutralizeParamName('maxWidth') // "_width" 10 | * neutralizeParamName('minWidth') // "_width" 11 | */ 12 | const neutralizeParamName = (paramName: string): string => { 13 | return paramName.replace(/^min|max/, '_') 14 | } 15 | 16 | /** 17 | * Determines whether two given breakpoints can be merged. 18 | * Assures non-compatible breakpoints are not prompted to 19 | * be merged during the area params composition. 20 | */ 21 | export default function shouldCombineBreakpoints( 22 | breakpointA: Breakpoint, 23 | breakpointB: Breakpoint, 24 | ): boolean { 25 | const allParams = Object.keys(breakpointA).concat(Object.keys(breakpointB)) 26 | 27 | return allParams.every((pristineParamName, index) => { 28 | const paramName = neutralizeParamName(pristineParamName) 29 | const prevParamName = neutralizeParamName(allParams[index - 1] || paramName) 30 | 31 | return paramName === prevParamName 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/withBreakpoints/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './withBreakpoints' 2 | export * from './withBreakpoints' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/withBreakpoints/matchMedia.mock.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable-rule no-implicit-dependencies */ 2 | import matchMediaMock from 'match-media-mock' 3 | 4 | const mock = matchMediaMock.create() 5 | 6 | mock.setConfig({ 7 | type: 'screen', 8 | height: window.innerHeight, 9 | width: window.innerWidth, 10 | }) 11 | 12 | window.matchMedia = mock 13 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/withBreakpoints/withBreakpoints.spec.ts: -------------------------------------------------------------------------------- 1 | import './matchMedia.mock' 2 | import withBreakpoints from './withBreakpoints' 3 | 4 | describe('withBreakpoints', () => { 5 | describe('given existing breakpoints', () => { 6 | it('should return value associated with matching breakpoint', () => { 7 | expect( 8 | withBreakpoints({ 9 | xs: 'extra small', 10 | md: 'medium', 11 | lg: 'large', 12 | }), 13 | ).toEqual('large') 14 | }) 15 | 16 | it('should return the default value when no breakpoints match', () => { 17 | expect( 18 | withBreakpoints( 19 | { 20 | md: 'medium', 21 | }, 22 | 'default', 23 | ), 24 | ).toEqual('default') 25 | }) 26 | 27 | it('should return "undefined" when not given default value', () => { 28 | expect( 29 | withBreakpoints({ 30 | md: 'medium', 31 | }), 32 | ).toBeUndefined() 33 | }) 34 | }) 35 | 36 | describe('given non-existing breakpoints', () => { 37 | it('should print console warning', () => { 38 | const consoleSpy = jest.spyOn(console, 'warn') 39 | withBreakpoints({ 40 | foo: 'bar', 41 | }) 42 | expect(console.warn).toHaveBeenCalledTimes(1) 43 | consoleSpy.mockRestore() 44 | }) 45 | 46 | it('should return default value when given', () => { 47 | expect( 48 | withBreakpoints( 49 | { 50 | foo: 'bar', 51 | }, 52 | 'default', 53 | ), 54 | ).toEqual('default') 55 | }) 56 | 57 | it('should return "undefined" when not given default value', () => { 58 | expect( 59 | withBreakpoints({ 60 | foo: 'bar', 61 | }), 62 | ).toBeUndefined() 63 | }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/breakpoints/withBreakpoints/withBreakpoints.ts: -------------------------------------------------------------------------------- 1 | import Layout from '../../../Layout' 2 | import { Breakpoints } from '../../../const/defaultOptions' 3 | import createMediaQuery from '../../styles/createMediaQuery' 4 | 5 | const createWithBreakpoints = (breakpoints: Breakpoints) => { 6 | const existingBreakpoints = Object.keys(breakpoints) 7 | 8 | return (breakpointsMap: Record, defaultValue?: T): T => { 9 | const breakpointMatch = Object.keys(breakpointsMap) 10 | .filter((breakpointName) => { 11 | const hasBreakpoint = existingBreakpoints.includes(breakpointName) 12 | 13 | if (!hasBreakpoint) { 14 | console.warn( 15 | 'useBreakpoints: Breakpoint "%s" is not found. Add it via "Layout.configure()", or use one of the existing breakpoints (%s).', 16 | breakpointName, 17 | existingBreakpoints.join(', '), 18 | ) 19 | } 20 | 21 | return hasBreakpoint 22 | }) 23 | .find((breakpointName) => { 24 | const breakpoint: Breakpoints | undefined = breakpoints[breakpointName] 25 | const mediaQuery = createMediaQuery(breakpoint, 'only') 26 | return breakpoint && matchMedia(mediaQuery).matches 27 | }) 28 | 29 | return breakpointMatch ? breakpointsMap[breakpointMatch] : defaultValue 30 | } 31 | } 32 | 33 | const withBreakpoints = createWithBreakpoints(Layout.breakpoints) 34 | 35 | export default withBreakpoints 36 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/compose/compose.spec.ts: -------------------------------------------------------------------------------- 1 | import compose from './compose' 2 | 3 | describe('compose', () => { 4 | it('composes list of functions from right to left', () => { 5 | const func = compose( 6 | (arr: string[]) => arr.reverse(), 7 | (str: string) => str.split(' '), 8 | (str: string) => str.toUpperCase(), 9 | ) 10 | 11 | const funcRes = func('Lorem ipsum') 12 | expect(funcRes).toEqual(['IPSUM', 'LOREM']) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/compose/compose.ts: -------------------------------------------------------------------------------- 1 | type Func = (...args: any[]) => any 2 | 3 | /** 4 | * Returns a functional composition of the given functions. 5 | * Applies no currying. 6 | */ 7 | export default function compose(...funcs: Func[]) { 8 | return funcs.reduce((f, g) => (...args: any[]) => f(g(...args))) 9 | } 10 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/compose/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './compose' 2 | export * from './compose' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/invariant/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './invariant' 2 | export * from './invariant' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/invariant/invariant.spec.ts: -------------------------------------------------------------------------------- 1 | import invariant from './invariant' 2 | 3 | describe('invariant', () => { 4 | describe('given predicate is satisfied', () => { 5 | it('should not throw any errors', () => { 6 | const run = () => invariant(true, 'You should not see this') 7 | expect(run).not.toThrow() 8 | }) 9 | }) 10 | 11 | describe('given predicate is not satisfied', () => { 12 | it('should throw an error with the correct message', () => { 13 | const errorMessage = 'Error message' 14 | const run = () => invariant(false, errorMessage) 15 | 16 | expect(run).toThrowError(errorMessage) 17 | }) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/invariant/invariant.ts: -------------------------------------------------------------------------------- 1 | export default function invariant(variable: any, message: string): void { 2 | if (!variable) { 3 | throw new Error(message) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/isset/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './isset' 2 | export * from './isset' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/isset/isset.spec.ts: -------------------------------------------------------------------------------- 1 | import isset from './isset' 2 | 3 | describe('isset', () => { 4 | describe('given a set variable', () => { 5 | it('should return true', () => { 6 | expect(isset(0)).toBe(true) 7 | expect(isset('')).toBe(true) 8 | expect(isset('a')).toBe(true) 9 | expect(isset([])).toBe(true) 10 | expect(isset({})).toBe(true) 11 | expect(isset(() => 'foo')).toBe(true) 12 | }) 13 | }) 14 | 15 | describe('given null', () => { 16 | it('should return false', () => { 17 | expect(isset(null)).toBe(false) 18 | }) 19 | }) 20 | 21 | describe('given undefined', () => { 22 | it('should return false', () => { 23 | expect(isset(undefined)).toBe(false) 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/isset/isset.ts: -------------------------------------------------------------------------------- 1 | export default function isset(variable: any): boolean { 2 | return typeof variable !== 'undefined' && variable !== null 3 | } 4 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/memoizeWith/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './memoizeWith' 2 | export * from './memoizeWith' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/memoizeWith/memoizeWith.spec.ts: -------------------------------------------------------------------------------- 1 | import memoizeWith from './memoizeWith' 2 | 3 | const factorial = (num: number) => { 4 | return Array(num) 5 | .fill(0) 6 | .map((_, i) => i + 1) 7 | .reduce((acc, n) => acc * n, 1) 8 | } 9 | 10 | const factorialMock = jest.fn(factorial) 11 | 12 | describe('memoizeWith', () => { 13 | describe('given memoized a function', () => { 14 | let memoizedFactorial: typeof factorial 15 | 16 | beforeAll(() => { 17 | const memoizeWithIdentity = memoizeWith((n) => 18 | String(n), 19 | ) 20 | memoizedFactorial = memoizeWithIdentity(factorialMock) 21 | }) 22 | 23 | afterEach(() => { 24 | factorialMock.mockClear() 25 | }) 26 | 27 | it('should return the result when called', () => { 28 | expect(memoizedFactorial(5)).toEqual(120) 29 | expect(factorialMock).toBeCalledTimes(1) 30 | }) 31 | 32 | it('should return memoized result when given the same arguments', () => { 33 | memoizedFactorial(5) 34 | memoizedFactorial(5) 35 | memoizedFactorial(5) 36 | expect(factorialMock).toBeCalledTimes(0) 37 | }) 38 | 39 | it('should execute anew for each unique set of arguments', () => { 40 | memoizedFactorial(3) 41 | memoizedFactorial(4) 42 | expect(factorialMock).toBeCalledTimes(2) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/memoizeWith/memoizeWith.ts: -------------------------------------------------------------------------------- 1 | const memoizeWith = any>( 2 | saltGenerator: (...args: Parameters) => string, 3 | ) => { 4 | const cache: Record = {} 5 | 6 | return (func: F) => 7 | function(...args: Parameters): ReturnType { 8 | const key = saltGenerator(...args) 9 | 10 | if (!(key in cache)) { 11 | cache[key] = func(...args) 12 | } 13 | 14 | return cache[key] 15 | } 16 | } 17 | 18 | export default memoizeWith 19 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/pop/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './pop' 2 | export * from './pop' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/pop/pop.spec.ts: -------------------------------------------------------------------------------- 1 | import pop from '../pop' 2 | 3 | describe('pop', () => { 4 | describe('given an empty array', () => { 5 | const array: any[] = [] 6 | let result: string[] 7 | 8 | beforeAll(() => { 9 | result = pop(array) 10 | }) 11 | 12 | it('should return a new empty array', () => { 13 | expect(result).toEqual([]) 14 | }) 15 | 16 | it('should not mutate the original array', () => { 17 | expect(array).toEqual([]) 18 | }) 19 | }) 20 | 21 | describe('given an array with a single member', () => { 22 | const array = ['area'] 23 | let result: string[] 24 | 25 | beforeAll(() => { 26 | result = pop(array) 27 | }) 28 | 29 | it('should return a new array with the last member removed', () => { 30 | expect(result).toEqual([]) 31 | }) 32 | 33 | it('should not mutate the original array', () => { 34 | expect(array).toEqual(['area']) 35 | }) 36 | }) 37 | 38 | describe('given an array with multiple members', () => { 39 | const array = [1, 2, 3] 40 | let result: number[] 41 | 42 | beforeAll(() => { 43 | result = pop(array) 44 | }) 45 | 46 | it('should return a new array with the last member removed', () => { 47 | expect(result).toEqual([1, 2]) 48 | }) 49 | 50 | it('should not mutate the original array', () => { 51 | expect(array).toEqual([1, 2, 3]) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/pop/pop.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the shallow copy of the given array with 3 | * the last element removed. 4 | */ 5 | export default function pop(list: T[]): T[] { 6 | return list.slice(0, list.length - 1) 7 | } 8 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/throttle/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './throttle' 2 | export * from './throttle' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/throttle/throttle.spec.ts: -------------------------------------------------------------------------------- 1 | import throttle from './throttle' 2 | 3 | describe('throttle', () => { 4 | let payload: number = 0 5 | 6 | afterEach(() => { 7 | payload = 0 8 | }) 9 | 10 | const func = (amount: number = 1) => { 11 | payload += amount 12 | } 13 | const throttledFunc = throttle(func, 50) 14 | 15 | it('should not be called more than once per interval', (done) => { 16 | throttledFunc() // 1 (leading) 17 | throttledFunc() // ignore 18 | setTimeout(throttledFunc, 30) // ignore 19 | setTimeout(throttledFunc, 60) // 2 20 | setTimeout(throttledFunc, 70) // 3 (trailing) 21 | 22 | setTimeout( 23 | () => { 24 | expect(payload).toBe(3) 25 | done() 26 | }, 27 | // Await for last call timestamp + interval 28 | // for the trailing function call. 29 | 120, 30 | ) 31 | }) 32 | 33 | it('should preserve original call signature', () => { 34 | throttledFunc(5) 35 | setTimeout(() => { 36 | expect(payload).toBe(5) 37 | }, 50) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/throttle/throttle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Throttles a given function. 3 | * Implements both leading and trailing function calls. 4 | */ 5 | export default function throttle any>( 6 | func: F, 7 | interval: number, 8 | ) { 9 | let previous: number 10 | let queuedToRun: NodeJS.Timeout = null 11 | 12 | return function invoker(...args: Parameters): void { 13 | const now = Date.now() 14 | clearTimeout(queuedToRun) 15 | 16 | if (!previous || now - previous >= interval) { 17 | func.apply(null, args) 18 | previous = now 19 | } else { 20 | queuedToRun = setTimeout( 21 | invoker.bind(null, ...args), 22 | interval - (now - previous), 23 | ) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/warn/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './warn' 2 | export * from './warn' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/warn/warn.spec.ts: -------------------------------------------------------------------------------- 1 | import warn from './warn' 2 | 3 | describe('warn', () => { 4 | describe('warns when not satisfied predicate', () => { 5 | const values = [ 6 | ['0', 0], 7 | ['false', false], 8 | ['null', null], 9 | ['undefined', undefined], 10 | ] 11 | 12 | values.forEach(([name, value]) => { 13 | it(`when given ${name}`, () => { 14 | const consoleSpy = jest.spyOn(console, 'warn') 15 | const message = 'Warning message' 16 | 17 | warn(value, message) 18 | expect(consoleSpy).toBeCalledTimes(1) 19 | expect(consoleSpy).toBeCalledWith(message) 20 | 21 | consoleSpy.mockRestore() 22 | }) 23 | }) 24 | }) 25 | 26 | describe('does nothing when satisfies predicate', () => { 27 | const values = [ 28 | ['one', 1], 29 | ['true', true], 30 | ['object', {}], 31 | ['array', [] as any[]], 32 | ] 33 | 34 | values.forEach(([name, value]) => { 35 | it(`when given ${name}`, () => { 36 | const consoleSpy = jest.spyOn(console, 'warn') 37 | 38 | warn(value, 'Foo') 39 | expect(consoleSpy).not.toBeCalled() 40 | 41 | consoleSpy.mockRestore() 42 | }) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/functions/warn/warn.ts: -------------------------------------------------------------------------------- 1 | export default function warn(predicate: any, message: string) { 2 | if (!predicate) { 3 | console.warn(message) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/math/transformNumeric.spec.ts: -------------------------------------------------------------------------------- 1 | import Layout from '../../Layout' 2 | import transformNumeric from './transformNumeric' 3 | 4 | describe('transformNumeric', () => { 5 | describe('given arbitrary number rather than 0', () => { 6 | describe('and using default measurement unit', () => { 7 | it('should suffix numeric value with the measurement unit', () => { 8 | expect(transformNumeric(5)).toBe('5px') 9 | }) 10 | }) 11 | 12 | describe('and using custom measurement unit', () => { 13 | beforeAll(() => { 14 | Layout.configure({ 15 | defaultUnit: 'rem', 16 | }) 17 | }) 18 | 19 | it('should suffix numeric value with the custom measurement unit', () => { 20 | expect(transformNumeric(3)).toBe('3rem') 21 | }) 22 | }) 23 | }) 24 | 25 | describe('given a string', () => { 26 | it('should return the string as-is', () => { 27 | expect(transformNumeric('2vh')).toBe('2vh') 28 | }) 29 | }) 30 | 31 | describe('given a numeric 0', () => { 32 | it('should return explicit "0"', () => { 33 | expect(transformNumeric(0)).toBe('0') 34 | }) 35 | }) 36 | 37 | describe('given a string zero "0"', () => { 38 | it('should return the string as-is', () => { 39 | expect(transformNumeric('0')).toBe('0') 40 | }) 41 | }) 42 | 43 | describe('given no input', () => { 44 | it('should return an empty string', () => { 45 | expect(transformNumeric()).toBe('') 46 | expect(transformNumeric('')).toBe('') 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/math/transformNumeric.ts: -------------------------------------------------------------------------------- 1 | import Layout from '../../Layout' 2 | import isset from '../functions/isset' 3 | 4 | export default function transformNumeric(value?: number | string): string { 5 | if (!isset(value)) { 6 | return '' 7 | } 8 | 9 | /** 10 | * Suffix numeric value with the default unit. 11 | * Accept explicit (string) value as-is. 12 | * 13 | * When given value is zero then its generated as it is, no suffix is attached 14 | */ 15 | const suffix = 16 | typeof value === 'number' && value !== 0 ? Layout.defaultUnit : '' 17 | 18 | return `${value}${suffix}` 19 | } 20 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/capitalize/capitalize.spec.ts: -------------------------------------------------------------------------------- 1 | import capitalize from './capitalize' 2 | 3 | describe('capitalize', () => { 4 | describe('given an arbitrary string', () => { 5 | it('should capitalize the given string', () => { 6 | expect(capitalize('foo')).toBe('Foo') 7 | }) 8 | }) 9 | 10 | describe('given already capitalized string', () => { 11 | it('should return the string as-is', () => { 12 | expect(capitalize('Foo')).toBe('Foo') 13 | }) 14 | }) 15 | 16 | describe('given a string with in-string capital letters', () => { 17 | let result: string 18 | 19 | beforeAll(() => { 20 | result = capitalize('fooBarDoe') 21 | }) 22 | 23 | it('should capitalize the string', () => { 24 | expect(result).toMatch(/^F/) 25 | }) 26 | 27 | it('should preserve existing capital letters', () => { 28 | expect(result).toBe('FooBarDoe') 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/capitalize/capitalize.ts: -------------------------------------------------------------------------------- 1 | export default function capitalize(str: string): string { 2 | return str.replace(/^./, (firstLetter) => firstLetter.toUpperCase()) 3 | } 4 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/capitalize/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './capitalize' 2 | export * from './capitalize' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/getPrefix/getPrefix.spec.ts: -------------------------------------------------------------------------------- 1 | import getPrefix from './getPrefix' 2 | 3 | describe('getPrefix', () => { 4 | describe('given a string with supported prefix', () => { 5 | it('should return the prefix', () => { 6 | expect(getPrefix('maxHeight')).toBe('max') 7 | expect(getPrefix('minResolution')).toBe('min') 8 | }) 9 | }) 10 | 11 | describe('given a string with a supported prefix within a string', () => { 12 | it('should ignore in-string matched and return an empty string', () => { 13 | expect(getPrefix('aminmaxWidth')).toBe('') 14 | }) 15 | }) 16 | 17 | describe('given an arbitrary string', () => { 18 | it('should return an empty string', () => { 19 | expect(getPrefix('fooBar')).toBe('') 20 | expect(getPrefix('abcDef')).toBe('') 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/getPrefix/getPrefix.ts: -------------------------------------------------------------------------------- 1 | export default function getPrefix(str: string): string { 2 | const prompt = str.match(/^(min|max)/) 3 | return prompt ? prompt[0] : '' 4 | } 5 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/getPrefix/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './getPrefix' 2 | export * from './getPrefix' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/hashString/hashString.spec.ts: -------------------------------------------------------------------------------- 1 | import hashString from './hashString' 2 | 3 | describe('hashString', () => { 4 | describe('given a string with an arbitrary value', () => { 5 | it('should return a hash based on the given string', () => { 6 | const input = 'template:header,content,footer' 7 | 8 | expect(hashString(input)).toBe(1927731245) 9 | expect(hashString(input)).toBe(1927731245) 10 | expect(hashString('templateMd:header,content,footer')).toBe(1323128868) 11 | }) 12 | }) 13 | 14 | describe('given an empty string', () => { 15 | it('should return explicit 0', () => { 16 | expect(hashString('')).toBe(0) 17 | }) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/hashString/hashString.ts: -------------------------------------------------------------------------------- 1 | export default function hashString(str: string): number { 2 | const { length } = str 3 | let hash = 0 4 | let i = 0 5 | 6 | if (length > 0) { 7 | while (i < length) { 8 | hash = ((hash << 5) - hash + str.charCodeAt(i++)) | 0 9 | } 10 | } 11 | 12 | return hash 13 | } 14 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/hashString/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './hashString' 2 | export * from './hashString' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/isAreaName/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './isAreaName' 2 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/isAreaName/isAreaName.spec.ts: -------------------------------------------------------------------------------- 1 | import isAreaName from './isAreaName' 2 | 3 | describe('isAreaName', () => { 4 | describe('given a valid string value', () => { 5 | it('should return true', () => { 6 | expect(isAreaName('footer')).toBe(true) 7 | }) 8 | }) 9 | 10 | describe('given a numeric value', () => { 11 | it('should return false', () => { 12 | expect(isAreaName('100px')).toBe(false) 13 | expect(isAreaName('2fr')).toBe(false) 14 | }) 15 | }) 16 | 17 | describe('given a reserved keyword', () => { 18 | it('should return false', () => { 19 | expect(isAreaName('/')).toBe(false) 20 | expect(isAreaName('auto')).toBe(false) 21 | }) 22 | }) 23 | 24 | describe('given a dot placeholder', () => { 25 | describe('given a single dot character', () => { 26 | it('should return false', () => { 27 | expect(isAreaName('.')).toBe(false) 28 | }) 29 | }) 30 | 31 | describe('given a sequence of dots', () => { 32 | it('should return false', () => { 33 | expect(isAreaName('..')).toBe(false) 34 | expect(isAreaName('....')).toBe(false) 35 | expect(isAreaName('.......')).toBe(false) 36 | }) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/isAreaName/isAreaName.ts: -------------------------------------------------------------------------------- 1 | export const keywords = [ 2 | // Dot symbol (and its sequence) is a valid placeholder, 3 | // but not a valid CSS Grid area name. 4 | /\.+/, 5 | // Numbers may be present in `grid-template` definition 6 | // and describe dimensions of rows/columns. 7 | /^[0-9]/, 8 | // Slash is a special symbol used to declare dimensions 9 | // for columns. 10 | '/', 11 | // "auto" is a reserved keyword to describe an automatic 12 | // dimension value when sizing rows/columns. 13 | 'auto', 14 | ] 15 | 16 | /** 17 | * Determines if a given string is a valid CSS Grid area name. 18 | * Takes into account row/column dimensions and reserved 19 | * keywords used in the `grid-template` definition. 20 | */ 21 | export default function isAreaName(areaName: string): boolean { 22 | return keywords.every((keyword) => { 23 | return keyword instanceof RegExp 24 | ? !keyword.test(areaName) 25 | : areaName !== keyword 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/parsePropName/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './parsePropName' 2 | export * from './parsePropName' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/parsePropName/parsePropName.ts: -------------------------------------------------------------------------------- 1 | import { BreakpointBehavior } from '../../../const/defaultOptions' 2 | import Layout from '../../../Layout' 3 | import toLowerCaseFirst from '../toLowerCaseFirst' 4 | import capitalize from '../capitalize' 5 | 6 | export interface Props { 7 | [propName: string]: any 8 | } 9 | 10 | export interface ParsedProp { 11 | originPropName: string 12 | purePropName: string 13 | breakpoint: ParsedBreakpoint 14 | behavior: BreakpointBehavior 15 | } 16 | 17 | export interface ParsedBreakpoint { 18 | name: string 19 | isDefault: boolean 20 | } 21 | 22 | /** 23 | * Returns a parsed prop summary, which includes pure prop name, 24 | * an optional breakpoint name and breakpoint behavior. 25 | * 26 | * \w+(?<=(sm)?(only)?)$ 27 | * This RegExp also works well. May consider implementing once 28 | * lookbehind is supported everywhere. 29 | */ 30 | export default function parsePropName(originPropName: string): ParsedProp { 31 | const joinedBreakpointNames = Object.keys(Layout.breakpoints) 32 | .map(capitalize) 33 | .join('|') 34 | const joinedBehaviors = ['down', 'only'].map(capitalize).join('|') 35 | const breakpointExp = new RegExp(`(${joinedBreakpointNames})$`, 'g') 36 | const behaviorExp = new RegExp(`(${joinedBehaviors})$`, 'g') 37 | 38 | const behaviorMatch = originPropName.match(behaviorExp) 39 | const behavior = behaviorMatch ? behaviorMatch[0] : '' 40 | const breakpointMatch = originPropName 41 | .replace(behavior, '') 42 | .match(breakpointExp) 43 | const breakpointName = breakpointMatch ? breakpointMatch[0] : '' 44 | const purePropName = originPropName 45 | .replace(breakpointName, '') 46 | .replace(behavior, '') 47 | 48 | /** 49 | * Get normalized breakpoint name. 50 | * When a breakpoint name is a part of the prop name, covert it first letter 51 | * to lowercase to match the layout options. Otherwise, take the default 52 | * breakpoint name. 53 | */ 54 | const normalizedBreakpointName = breakpointName 55 | ? toLowerCaseFirst(breakpointName) 56 | : Layout.defaultBreakpointName 57 | 58 | const isDefaultBreakpoint = 59 | normalizedBreakpointName === Layout.defaultBreakpointName 60 | 61 | return { 62 | originPropName, 63 | purePropName, 64 | behavior: behavior 65 | ? toLowerCaseFirst(behavior) 66 | : Layout.defaultBehavior, 67 | breakpoint: { 68 | name: normalizedBreakpointName, 69 | isDefault: isDefaultBreakpoint, 70 | }, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/sanitizeTemplateArea/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './sanitizeTemplateArea' 2 | export * from './sanitizeTemplateArea' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/sanitizeTemplateArea/sanitizeTemplateArea.spec.ts: -------------------------------------------------------------------------------- 1 | import sanitizeTemplateArea from './sanitizeTemplateArea' 2 | 3 | describe('sanitizeTemplateArea', () => { 4 | describe('given an arbitrary template string', () => { 5 | it('should enforce single quotes for new lines', () => { 6 | expect(sanitizeTemplateArea('foo')).toEqual(`'foo'`) 7 | expect(sanitizeTemplateArea('foo bar')).toEqual(`'foo bar'`) 8 | }) 9 | }) 10 | 11 | describe('given a template string with duplicate quotes', () => { 12 | it('should return a parsed template string with deduplicated quotes', () => { 13 | expect(sanitizeTemplateArea("'foo'")).toEqual(`'foo'`) 14 | expect(sanitizeTemplateArea("'foo bar'")).toEqual(`'foo bar'`) 15 | }) 16 | }) 17 | 18 | describe('given "grid-template" syntax', () => { 19 | it('should parse single area definition', () => { 20 | expect(sanitizeTemplateArea('foo bar 100px')).toEqual(`'foo bar' 100px`) 21 | }) 22 | 23 | it('should parse multi-area definition', () => { 24 | expect(sanitizeTemplateArea('first second 2fr')).toEqual( 25 | `'first second' 2fr`, 26 | ) 27 | }) 28 | 29 | it('should return "grid-template-columns" dimensions without quoutes', () => { 30 | expect(sanitizeTemplateArea('/ 200px auto 1fr')).toEqual( 31 | `/ 200px auto 1fr`, 32 | ) 33 | }) 34 | }) 35 | 36 | describe('given a template string with placeholder dots', () => { 37 | it('should return a parsed template string including dots', () => { 38 | expect(sanitizeTemplateArea('first . second ... third')).toEqual( 39 | `'first . second ... third'`, 40 | ) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/sanitizeTemplateArea/sanitizeTemplateArea.ts: -------------------------------------------------------------------------------- 1 | import compose from '../../functions/compose' 2 | import isAreaName from '../isAreaName' 3 | 4 | type SanitizeTemplateArea = (str: string) => string 5 | 6 | /** 7 | * Joins a given template string fragments into a valid template string. 8 | * Appends any row/column dimensions after the enclosing single quote 9 | * character to have a valid `grid-template` syntax. 10 | */ 11 | const joinTemplateFragments = (fragments: string[]): string => { 12 | const areas: string[] = [] 13 | const suffixes: string[] = [] 14 | 15 | fragments.forEach((areaName) => { 16 | if (isAreaName(areaName) || /\.+/.test(areaName)) { 17 | areas.push(areaName) 18 | } else { 19 | suffixes.push(areaName) 20 | } 21 | }) 22 | 23 | // Wraps areas string in single quote per CSS spec 24 | const joinedAreas = areas.length > 0 ? `'${areas.join(' ')}'` : '' 25 | const joinedSuffixes = suffixes.join(' ') 26 | 27 | // Ensures row/column dimensions follow areas list after its been 28 | // wrapped in single quotes. 29 | return [joinedAreas, joinedSuffixes].filter(Boolean).join(' ') 30 | } 31 | 32 | /** 33 | * Sanitizes a given `grid-template-areas` string. 34 | * Trims whitespaces, deduplicates quotes and wraps each line 35 | * in single quotes to be CSS-compliant. 36 | */ 37 | const sanitizeTemplateArea: SanitizeTemplateArea = compose( 38 | joinTemplateFragments, 39 | (area: string): string[] => area.split(' '), 40 | (area: string): string => area.replace(/'+/gm, ''), 41 | (area: string): string => area.trim(), 42 | ) 43 | 44 | export default sanitizeTemplateArea 45 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/sanitizeTemplateString/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './sanitizeTemplateString' 2 | export * from './sanitizeTemplateString' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/sanitizeTemplateString/sanitizeTemplateString.ts: -------------------------------------------------------------------------------- 1 | import compose from '../../functions/compose' 2 | import isAreaName from '../isAreaName' 3 | 4 | type SanitizeTemplateString = (str: string) => string[] 5 | 6 | /** 7 | * Returns an array of unique normalized grid area names 8 | * from the given template string. Any member of the returned list 9 | * is later evolved into a React component. 10 | */ 11 | const sanitizeTemplateString: SanitizeTemplateString = compose( 12 | (list: string[]): string[] => list.sort(), 13 | 14 | // Deduplicate area strings 15 | (list: string[]): string[] => Array.from(new Set(list)), 16 | 17 | // Filter out "template" row/columns sizes 18 | (arr: string[]): string[] => arr.filter(isAreaName), 19 | 20 | // Filter out empty area strings 21 | (arr: string[]): string[] => arr.filter(Boolean), 22 | 23 | // Split into a list of areas 24 | (str: string): string[] => str.split(' '), 25 | 26 | // Deduplicate multiple spaces 27 | (str: string): string => str.replace(/\s+/g, ' '), 28 | 29 | // Replace new lines and single quotes with spaces 30 | (str: string): string => str.replace(/\r?\n|\'+/g, ' '), 31 | ) 32 | 33 | export default sanitizeTemplateString 34 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/toDashedString/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './toDashedString' 2 | export * from './toDashedString' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/toDashedString/toDashedString.spec.ts: -------------------------------------------------------------------------------- 1 | import toDashedString from './toDashedString' 2 | 3 | describe('toDashedString', () => { 4 | describe('given a cammelCase string', () => { 5 | it('should convert the string to kebab-case', () => { 6 | expect(toDashedString('loremIpsum')).toBe('lorem-ipsum') 7 | expect(toDashedString('loremIpsumDolor')).toBe('lorem-ipsum-dolor') 8 | }) 9 | }) 10 | 11 | describe('given a string without a capital letter', () => { 12 | it('should return the string as-is', () => { 13 | expect(toDashedString('lorem')).toBe('lorem') 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/toDashedString/toDashedString.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts given cammelCase string into kebab-case. 3 | * @example 4 | * toDashedString('fooBar') 5 | * @returns "foo-bar" 6 | */ 7 | export default function toDashedString(str: string): string { 8 | return str.replace(/[A-Z]/g, (capitalLetter) => { 9 | return `-${capitalLetter}`.toLowerCase() 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/toLowerCaseFirst/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './toLowerCaseFirst' 2 | export * from './toLowerCaseFirst' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/toLowerCaseFirst/toLowerCaseFirst.spec.ts: -------------------------------------------------------------------------------- 1 | import toLowerCaseFirst from './toLowerCaseFirst' 2 | 3 | describe('toLowerCaseFirst', () => { 4 | describe('given a capitalized string', () => { 5 | it('should convert the first letter to lowercase', () => { 6 | expect(toLowerCaseFirst('Foo')).toBe('foo') 7 | expect(toLowerCaseFirst('FooBar')).toBe('fooBar') 8 | }) 9 | }) 10 | 11 | describe('given a string with first latter already being lowercase', () => { 12 | it('should return the string as-is', () => { 13 | expect(toLowerCaseFirst('foo')).toBe('foo') 14 | expect(toLowerCaseFirst('fooBar')).toBe('fooBar') 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/strings/toLowerCaseFirst/toLowerCaseFirst.ts: -------------------------------------------------------------------------------- 1 | export default function toLowerCaseFirst(str: string): R { 2 | return (str.slice(0, 1).toLowerCase() + str.slice(1, str.length)) as any 3 | } 4 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/styles/applyStyles/applyStyles.ts: -------------------------------------------------------------------------------- 1 | import Layout from '../../../Layout' 2 | import { BreakpointBehavior } from '../../../const/defaultOptions' 3 | import propAliases from '../../../const/propAliases' 4 | import parsePropName, { 5 | Props, 6 | ParsedBreakpoint, 7 | } from '../../strings/parsePropName' 8 | import isset from '../../functions/isset' 9 | import createMediaQuery from '../createMediaQuery' 10 | 11 | /** 12 | * Generateds a single CSS string for the set of props, 13 | * value, and its responsive information. 14 | */ 15 | const createStyleString = ( 16 | cssProps: string[], 17 | propValue: any, 18 | parsedBreakpoint: ParsedBreakpoint, 19 | behavior: BreakpointBehavior, 20 | ) => { 21 | const styleString = cssProps 22 | .map((propName) => `${propName}:${String(propValue)};`) 23 | .join('') 24 | 25 | // Get a breakpoint dimensions based on the statically analyzed breakpoint. 26 | const breakpointOptions = Layout.breakpoints[parsedBreakpoint.name] 27 | 28 | // Wrap CSS rule in a media query only if its prop includes 29 | // a breakpoint and behavior different from the default ones. 30 | const shouldWrapInMediaQuery = 31 | breakpointOptions && 32 | !(parsedBreakpoint.isDefault && behavior === Layout.defaultBehavior) 33 | 34 | return shouldWrapInMediaQuery 35 | ? `@media ${createMediaQuery(breakpointOptions, behavior)} {${styleString}}` 36 | : styleString 37 | } 38 | 39 | /** 40 | * Transforms known prop aliases to CSS rules for the given props. 41 | */ 42 | export default function applyStyles(pristineProps: Props): string { 43 | return ( 44 | Object.keys(pristineProps) 45 | // Parse each prop to include "breakpoint" and "behavior" 46 | .map(parsePropName) 47 | // Filter out props that are not included in prop aliases 48 | .filter(({ purePropName }) => propAliases.hasOwnProperty(purePropName)) 49 | // Filter out props with "undefined" or "null" as value 50 | .filter(({ originPropName }) => isset(pristineProps[originPropName])) 51 | // Map each prop to a CSS rule string 52 | .map(({ purePropName, originPropName, breakpoint, behavior }) => { 53 | const { props, transformValue } = propAliases[purePropName] 54 | const propValue = pristineProps[originPropName] 55 | const transformedPropValue = transformValue 56 | ? transformValue(propValue) 57 | : propValue 58 | 59 | return createStyleString( 60 | props, 61 | transformedPropValue, 62 | breakpoint, 63 | behavior, 64 | ) 65 | }) 66 | .join(' ') 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/styles/applyStyles/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './applyStyles' 2 | export * from './applyStyles' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/styles/createMediaQuery/createMediaQuery.spec.ts: -------------------------------------------------------------------------------- 1 | import createMediaQuery from './createMediaQuery' 2 | 3 | describe('createMediaQuery', () => { 4 | describe('given a breakpoint with "up" behavior', () => { 5 | let mediaQuery: string 6 | 7 | beforeAll(() => { 8 | mediaQuery = createMediaQuery( 9 | { 10 | minWidth: 500, 11 | maxWidth: 765, 12 | }, 13 | 'up', 14 | ) 15 | }) 16 | 17 | it('should not have any "max-" breakpoint properties', () => { 18 | expect(mediaQuery).not.toContain('maxWidth') 19 | }) 20 | 21 | it('should return a media query string with "min-" breakpoint properties', () => { 22 | expect(mediaQuery).toEqual('(min-width:500px)') 23 | }) 24 | }) 25 | 26 | describe('given a breakpoint with "down" behavior', () => { 27 | let mediaQuery: string 28 | 29 | beforeAll(() => { 30 | mediaQuery = createMediaQuery( 31 | { 32 | minWidth: 400, 33 | maxWidth: 565, 34 | minResolution: '300dpi', 35 | }, 36 | 'down', 37 | ) 38 | }) 39 | 40 | it('should not have any "min-" breakpoint properties', () => { 41 | expect(mediaQuery).not.toContain('minWidth') 42 | }) 43 | 44 | it('should return a media query string with "max-" breakpoint properties', () => { 45 | expect(mediaQuery).toEqual( 46 | '(max-width:565px) and (min-resolution:300dpi)', 47 | ) 48 | }) 49 | }) 50 | 51 | describe('given a breakpoint with "only" behavior', () => { 52 | let mediaQuery: string 53 | 54 | beforeAll(() => { 55 | mediaQuery = createMediaQuery( 56 | { 57 | minWidth: 768, 58 | maxWidth: 1120, 59 | orientation: 'landscape', 60 | }, 61 | 'only', 62 | ) 63 | }) 64 | 65 | it('should return a media query string with all breakpoint properties', () => { 66 | expect(mediaQuery).toContain( 67 | '(min-width:768px) and (max-width:1120px) and (orientation:landscape)', 68 | ) 69 | }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/styles/createMediaQuery/createMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Numeric, 3 | Breakpoint, 4 | BreakpointBehavior, 5 | } from '../../../const/defaultOptions' 6 | import transformNumeric from '../../math/transformNumeric' 7 | import normalizeQuery from '../../styles/normalizeQuery' 8 | import compose from '../../functions/compose' 9 | 10 | type MediaQueryPair = [string, Numeric] 11 | 12 | /** 13 | * Determines whether a given media query param should be added 14 | * to the media query string based on a breakpoint's behavior. 15 | */ 16 | const shouldAppendProperty = ( 17 | queryParam: string, 18 | behavior: BreakpointBehavior, 19 | ): boolean => { 20 | const [prefix, splitPropName] = queryParam.split('-') 21 | const isDimensionalProp = ['height', 'width'].includes(splitPropName) 22 | 23 | if (!isDimensionalProp) { 24 | return true 25 | } 26 | 27 | return ( 28 | (prefix === 'min' && ['up', 'only'].includes(behavior)) || 29 | (prefix === 'max' && ['down', 'only'].includes(behavior)) 30 | ) 31 | } 32 | 33 | const filterRelevantQueryParams = (behavior: BreakpointBehavior) => ( 34 | queryList: MediaQueryPair[], 35 | ): MediaQueryPair[] => { 36 | return queryList.filter(([queryParam]) => 37 | shouldAppendProperty(queryParam, behavior), 38 | ) 39 | } 40 | 41 | /** 42 | * Joins a given media query params list with the given transformer function. 43 | */ 44 | export const joinQueryList = (transformer: (pair: MediaQueryPair) => any) => ( 45 | queryList: MediaQueryPair[], 46 | ) => { 47 | return queryList.map(transformer).join(' and ') 48 | } 49 | 50 | export default function createMediaQuery( 51 | breakpoint: Breakpoint, 52 | behavior: BreakpointBehavior, 53 | ): string { 54 | return compose( 55 | joinQueryList(([dashedQueryProp, propValue]) => { 56 | return `(${dashedQueryProp}:${String(transformNumeric(propValue))})` 57 | }), 58 | filterRelevantQueryParams(behavior), 59 | normalizeQuery, 60 | )(breakpoint) 61 | } 62 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/styles/createMediaQuery/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './createMediaQuery' 2 | export * from './createMediaQuery' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/styles/normalizeQuery/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './normalizeQuery' 2 | export * from './normalizeQuery' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/styles/normalizeQuery/normalizeQuery.spec.ts: -------------------------------------------------------------------------------- 1 | import normalizeQuery from './normalizeQuery' 2 | 3 | describe('normalizeQuery', () => { 4 | describe('given a media query Object', () => { 5 | it('returns its [key, value] pairs', () => { 6 | expect( 7 | normalizeQuery({ 8 | minWidth: 120, 9 | maxAspectRatio: '3/4', 10 | }), 11 | ).toEqual([['min-width', 120], ['max-aspect-ratio', '3/4']]) 12 | }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/styles/normalizeQuery/normalizeQuery.ts: -------------------------------------------------------------------------------- 1 | import { Numeric, Breakpoint } from '../../../const/defaultOptions' 2 | import isset from '../../functions/isset' 3 | import toDashedString from '../../strings/toDashedString' 4 | 5 | /** 6 | * Normalizes given media query object to a list of [propName, propValue]. 7 | * @example 8 | * normalizeQuery({ minWidth: 120 }) 9 | * // [['min-width', 120]] 10 | */ 11 | export default function normalizeQuery( 12 | queryProps: Breakpoint, 13 | ): Array<[string, Numeric]> { 14 | return Object.entries(queryProps) 15 | .filter(([_, propValue]) => isset(propValue)) 16 | .map<[string, Numeric]>(([propName, propValue]) => [ 17 | toDashedString(propName), 18 | propValue, 19 | ]) 20 | } 21 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/templates/generateComponents/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './generateComponents' 2 | export * from './generateComponents' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/templates/getAreasList/getAreasList.spec.ts: -------------------------------------------------------------------------------- 1 | import Layout from '../../../Layout' 2 | import filterTemplateProps from '../parseTemplates/filterTemplateProps' 3 | import getAreasList from './getAreasList' 4 | 5 | const withProps = filterTemplateProps 6 | 7 | describe('getAreasList', () => { 8 | it('parses template props properly', () => { 9 | const areasList = getAreasList( 10 | withProps({ 11 | template: `a b`, 12 | }), 13 | ) 14 | 15 | expect(areasList).toEqual({ 16 | areas: ['a', 'b'], 17 | templates: [ 18 | { 19 | areas: ['a', 'b'], 20 | behavior: 'up', 21 | breakpoint: Layout.breakpoints.xs, 22 | }, 23 | ], 24 | }) 25 | }) 26 | 27 | it('returns proper areas list for "up" behavior', () => { 28 | const areasList = getAreasList( 29 | withProps({ 30 | template: `a b`, 31 | templateMd: `a b c`, 32 | }), 33 | ) 34 | 35 | expect(areasList).toEqual({ 36 | areas: ['a', 'b', 'c'], 37 | templates: [ 38 | { 39 | areas: ['a', 'b'], 40 | behavior: 'up', 41 | breakpoint: Layout.breakpoints.xs, 42 | }, 43 | { 44 | areas: ['a', 'b', 'c'], 45 | behavior: 'up', 46 | breakpoint: Layout.breakpoints.md, 47 | }, 48 | ], 49 | }) 50 | }) 51 | 52 | it('returns proper areas list for "down" behavior', () => { 53 | const areasList = getAreasList( 54 | withProps({ 55 | template: `a b`, 56 | templateMdDown: `c`, 57 | }), 58 | ) 59 | 60 | expect(areasList).toEqual({ 61 | areas: ['a', 'b', 'c'], 62 | templates: [ 63 | { 64 | areas: ['a', 'b'], 65 | behavior: 'up', 66 | breakpoint: Layout.breakpoints.xs, 67 | }, 68 | { 69 | areas: ['c'], 70 | behavior: 'down', 71 | breakpoint: Layout.breakpoints.md, 72 | }, 73 | ], 74 | }) 75 | }) 76 | 77 | it('returns proper areas list for "only" behavior', () => { 78 | const areasList = getAreasList( 79 | withProps({ 80 | template: `a`, 81 | templateMdOnly: `b c`, 82 | }), 83 | ) 84 | 85 | expect(areasList).toEqual({ 86 | areas: ['a', 'b', 'c'], 87 | templates: [ 88 | { 89 | areas: ['a'], 90 | behavior: 'up', 91 | breakpoint: Layout.breakpoints.xs, 92 | }, 93 | { 94 | areas: ['b', 'c'], 95 | behavior: 'only', 96 | breakpoint: Layout.breakpoints.md, 97 | }, 98 | ], 99 | }) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/templates/getAreasList/getAreasList.ts: -------------------------------------------------------------------------------- 1 | import { Breakpoint, BreakpointBehavior } from '../../../const/defaultOptions' 2 | import Layout from '../../../Layout' 3 | import parsePropName from '../../strings/parsePropName' 4 | 5 | export interface Template { 6 | breakpoint: Breakpoint 7 | behavior: BreakpointBehavior 8 | areas: string[] 9 | } 10 | 11 | export interface AreasList { 12 | areas: string[] 13 | templates: Template[] 14 | } 15 | 16 | export interface TemplateProps { 17 | [propName: string]: string[] 18 | } 19 | 20 | export default function getAreasList(templateProps: TemplateProps): AreasList { 21 | const areasList = Object.entries(templateProps).reduce( 22 | (acc, [templateName, templateAreas]) => { 23 | const { breakpoint, behavior } = parsePropName(templateName) 24 | const nextAreas = acc.areas.concat(templateAreas) 25 | const nextTemplates = acc.templates.concat({ 26 | breakpoint: Layout.breakpoints[breakpoint.name], 27 | behavior, 28 | areas: templateAreas, 29 | }) 30 | 31 | return { 32 | areas: nextAreas, 33 | templates: nextTemplates, 34 | } 35 | }, 36 | { 37 | areas: [], 38 | templates: [], 39 | }, 40 | ) 41 | 42 | const { areas, templates } = areasList 43 | 44 | return { 45 | areas: Array.from(new Set(areas)), 46 | templates, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/templates/getAreasList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './getAreasList' 2 | export * from './getAreasList' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/templates/parseTemplates/filterTemplateProps.spec.ts: -------------------------------------------------------------------------------- 1 | import filterTemplateProps, { TemplateProps } from './filterTemplateProps' 2 | 3 | describe('filterTemplateProps', () => { 4 | describe('given an object with arbitrary props', () => { 5 | let templateProps: TemplateProps 6 | 7 | beforeAll(() => { 8 | templateProps = filterTemplateProps({ 9 | template: 'first', 10 | templateOnly: 'three', 11 | templateMd: 'second', 12 | templateCols: true, 13 | templateBars: true, 14 | 15 | randomProp: 'yes', 16 | yetAnotherUknownProp: true, 17 | }) 18 | }) 19 | 20 | it('should ignore non-template props', () => { 21 | expect(templateProps).not.toContain('randomProp') 22 | expect(templateProps).not.toContain('yetAnotherUknownProp') 23 | }) 24 | 25 | it('should return template props', () => { 26 | expect(templateProps).toEqual({ 27 | template: ['first'], 28 | templateOnly: ['three'], 29 | templateMd: ['second'], 30 | }) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/templates/parseTemplates/filterTemplateProps.ts: -------------------------------------------------------------------------------- 1 | import parsePropName, { Props } from '../../strings/parsePropName' 2 | import sanitizeTemplateString from '../../strings/sanitizeTemplateString' 3 | 4 | export interface TemplateProps { 5 | [templateProp: string]: string[] 6 | } 7 | 8 | /** 9 | * Accepts a props object and filters it to include 10 | * only template-related prop:value pairs. 11 | */ 12 | const filterTemplateProps = (props: Props): TemplateProps => { 13 | return Object.keys(props) 14 | .filter((propName) => { 15 | const { purePropName } = parsePropName(propName) 16 | return ['areas', 'template'].includes(purePropName) 17 | }) 18 | .reduce((acc, propName) => { 19 | return { 20 | ...acc, 21 | [propName]: sanitizeTemplateString(props[propName]), 22 | } 23 | }, {}) 24 | } 25 | 26 | export default filterTemplateProps 27 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/templates/parseTemplates/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './parseTemplates' 2 | export * from './parseTemplates' 3 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/src/utils/templates/parseTemplates/parseTemplates.ts: -------------------------------------------------------------------------------- 1 | import filterTemplateProps, { TemplateProps } from './filterTemplateProps' 2 | import getAreasList, { AreasList } from '../getAreasList' 3 | import compose from '../../functions/compose' 4 | import memoizeWith from '../../functions/memoizeWith' 5 | import hashString from '../../strings/hashString' 6 | import { Props } from '../../strings/parsePropName' 7 | 8 | type ParseTemplates = (props: Props) => AreasList 9 | 10 | /** 11 | * Memoize areas generation based on the sanitized "templateProp:areas" pairs. 12 | * Alphabetical sorting of incoming template areas allows reproducible cache keys. 13 | * @todo `pairs` is an empty array sometimes. Should we handle it somehow? 14 | */ 15 | const memoizeProps = memoizeWith((templateProps: TemplateProps) => { 16 | const pairs = Object.entries(templateProps).reduce( 17 | (acc, [propName, templateAreas]) => { 18 | return acc.concat(`${propName}:${templateAreas.join()}`) 19 | }, 20 | [], 21 | ) 22 | 23 | return hashString(pairs.join()).toString() 24 | }) 25 | 26 | /** 27 | * Parses a given map of props and returns an areas map. 28 | */ 29 | const parseTemplates: ParseTemplates = compose( 30 | memoizeProps(getAreasList), 31 | filterTemplateProps, 32 | ) 33 | 34 | export default parseTemplates 35 | -------------------------------------------------------------------------------- /packages/atomic-layout-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "module": "es6", 5 | "target": "esnext", 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "noImplicitThis": false, 9 | "noImplicitAny": true, 10 | "esModuleInterop": true, 11 | "baseUrl": ".", 12 | "types": ["node", "jest"] 13 | }, 14 | "include": ["global.d.ts", "src/**/*"], 15 | "exclude": ["node_modules", "**/*.spec.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/atomic-layout-emotion/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Artem Zakharchenko 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. -------------------------------------------------------------------------------- /packages/atomic-layout-emotion/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const babelConfigPath = path.resolve(__dirname, '../..', 'babel.config.js') 4 | 5 | console.log( 6 | '@atomic-layout/emotion: loading babel config at "%s"', 7 | babelConfigPath, 8 | ) 9 | 10 | module.exports = { 11 | // Resolve the path otherwise it gets resolved relatively 12 | // to Storybook main entry module during the Storybook build. 13 | extends: babelConfigPath, 14 | plugins: [ 15 | [ 16 | 'transform-rename-import', 17 | { 18 | // Replace imports to "styled-components" with imports to emotion. 19 | // Applied to the modules imported from "atomic-layout" package. 20 | original: '^styled-components', 21 | replacement: '@emotion/styled', 22 | }, 23 | ], 24 | ], 25 | } 26 | -------------------------------------------------------------------------------- /packages/atomic-layout-emotion/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Atomic Layout (emotion)', 3 | verbose: true, 4 | roots: ['src', 'test'], 5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], 6 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 7 | transform: { 8 | '^.+\\.tsx?$': 'ts-jest', 9 | '^.+\\.(js|jsx)$': 'babel-jest', 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /packages/atomic-layout-emotion/logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/packages/atomic-layout-emotion/logo-full.png -------------------------------------------------------------------------------- /packages/atomic-layout-emotion/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Layout } from '@atomic-layout/core' 2 | export default Layout 3 | 4 | export { 5 | /* Components */ 6 | Box, 7 | Composition, 8 | Only, 9 | Visible, 10 | /* Hooks */ 11 | useMediaQuery, 12 | useViewportChange, 13 | useBreakpointChange, 14 | useResponsiveValue, 15 | useResponsiveProps, 16 | useResponsiveQuery, 17 | /* Utils */ 18 | query, 19 | makeResponsive, 20 | } from '../../atomic-layout/src' 21 | -------------------------------------------------------------------------------- /packages/atomic-layout-emotion/test/refs.spec.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Composition, Only, Visible } from '../src' 2 | import { createForwardRefTest } from '../../atomic-layout/test/createForwardRefTest' 3 | 4 | createForwardRefTest({ Box, Composition, Only, Visible }) 5 | -------------------------------------------------------------------------------- /packages/atomic-layout-emotion/test/ssr.spec.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | import { createSsrTest } from '../../atomic-layout/test/createSsrTest' 5 | 6 | createSsrTest(() => import('../src/index')) 7 | -------------------------------------------------------------------------------- /packages/atomic-layout-emotion/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "module": "es6", 5 | "target": "esnext", 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "emitDeclarationOnly": true, 9 | "declarationDir": "lib/types", 10 | "noImplicitThis": false, 11 | "noImplicitAny": true, 12 | "esModuleInterop": true, 13 | "jsx": "react", 14 | "baseUrl": ".", 15 | "types": ["jest"], 16 | "rootDirs": ["src", "../atomic-layout/src"] 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "**/*.spec.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/atomic-layout-emotion/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../atomic-layout/tslint.json"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/atomic-layout/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Artem Zakharchenko 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. -------------------------------------------------------------------------------- /packages/atomic-layout/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const babelConfigPath = path.resolve(__dirname, '../..', 'babel.config.js') 4 | 5 | console.log( 6 | '@atomic-layout/styled: loading babel config at "%s"', 7 | babelConfigPath, 8 | ) 9 | 10 | module.exports = { 11 | // Resolve the path otherwise it gets resolved relatively 12 | // to Storybook main entry module during the Storybook build. 13 | extends: babelConfigPath, 14 | } 15 | -------------------------------------------------------------------------------- /packages/atomic-layout/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Atomic Layout (styled-components)', 3 | verbose: true, 4 | roots: ['src', 'test'], 5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], 6 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 7 | transform: { 8 | '^.+\\.tsx?$': 'ts-jest', 9 | '^.+\\.(js|jsx)$': 'babel-jest', 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /packages/atomic-layout/logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kettanaito/atomic-layout/3df0c1c33739f627db72d03a005b4ee9c7b8aad6/packages/atomic-layout/logo-full.png -------------------------------------------------------------------------------- /packages/atomic-layout/src/components/Box.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, cleanup, getByText } from '@testing-library/react' 3 | import '@testing-library/jest-dom/extend-expect' 4 | import Box from './Box' 5 | 6 | describe('Box', () => { 7 | afterEach(cleanup) 8 | 9 | it('renders default Box', () => { 10 | const { container } = render(Content) 11 | const renderedBox = getByText(container, 'Content') 12 | 13 | expect(renderedBox).toHaveTextContent('Content') 14 | expect(renderedBox).toHaveStyle('display:block') 15 | expect(renderedBox).toHaveStyle('padding:10px') 16 | }) 17 | 18 | it('renders inline Box', () => { 19 | const { container } = render( 20 | 21 | Content 22 | , 23 | ) 24 | const renderedBox = getByText(container, 'Content') 25 | 26 | expect(renderedBox).toHaveTextContent('Content') 27 | expect(renderedBox).toHaveStyle('display:inline-block') 28 | }) 29 | 30 | it('supports flexbox display model', () => { 31 | const { container } = render( 32 | 33 | Content 34 | , 35 | ) 36 | const renderedBlock = getByText(container, 'Content') 37 | 38 | expect(renderedBlock).toHaveTextContent('Content') 39 | expect(renderedBlock).toHaveStyle('display:flex') 40 | expect(renderedBlock).toHaveStyle('align-items:center') 41 | }) 42 | 43 | it('supports inline flexbox display model', () => { 44 | const { container } = render( 45 | 46 | Content 47 | , 48 | ) 49 | const renderedBlock = getByText(container, 'Content') 50 | 51 | expect(renderedBlock).toHaveTextContent('Content') 52 | expect(renderedBlock).toHaveStyle('display:inline-flex') 53 | expect(renderedBlock).toHaveStyle('justify-content:center') 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/components/Box.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | import { BoxProps, applyStyles } from '@atomic-layout/core' 4 | 5 | const Box: React.FC = styled.div` 6 | display: ${({ flex, inline }) => 7 | flex 8 | ? inline 9 | ? 'inline-flex' 10 | : 'flex' 11 | : inline 12 | ? 'inline-block' 13 | : 'block'}; 14 | 15 | && { 16 | ${applyStyles}; 17 | } 18 | ` 19 | 20 | Box.displayName = 'Box' 21 | 22 | export default Box 23 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/components/Composition.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | import { 4 | BoxProps, 5 | CompositionProps, 6 | CompositionRenderProp, 7 | AreaComponent, 8 | parseTemplates, 9 | generateComponents, 10 | applyStyles, 11 | warn, 12 | } from '@atomic-layout/core' 13 | import Box from './Box' 14 | import { withPlaceholder } from '../utils/withPlaceholder' 15 | import { forwardRef } from '../utils/forwardRef' 16 | 17 | const CompositionWrapper = styled.div` 18 | && { 19 | ${applyStyles}; 20 | display: ${({ inline }) => (inline ? 'inline-grid' : 'grid')}; 21 | } 22 | ` 23 | 24 | const createAreaComponent = (areaName: string): AreaComponent => 25 | forwardRef((props: BoxProps, ref) => { 26 | return 27 | }) 28 | 29 | const Composition = forwardRef( 30 | ({ children, ...restProps }, ref) => { 31 | const areasList = parseTemplates(restProps) 32 | 33 | // Memoize areas generation so parental updates do not re-generate areas, 34 | // making area components preserve their internal state. 35 | const Areas = React.useMemo(() => { 36 | return generateComponents(areasList, createAreaComponent, withPlaceholder) 37 | }, [areasList]) 38 | 39 | const hasAreaComponents = Object.keys(Areas).length > 0 40 | const childrenType = typeof children 41 | const hasChildrenFunction = childrenType === 'function' 42 | 43 | // Warn when provided "areas"/"template" props, but didn't use a render prop pattern. 44 | warn( 45 | !(hasAreaComponents && !hasChildrenFunction), 46 | `Failed to render 'Composition' with template areas ["${Object.keys( 47 | Areas, 48 | ).join( 49 | '", "', 50 | )}"]: expected children to be a function, but got: ${childrenType}. Please provide render function as children, or remove assigned template props (\`areas\`/\`template\`).`, 51 | ) 52 | 53 | return ( 54 | 55 | {hasAreaComponents && hasChildrenFunction 56 | ? (children as CompositionRenderProp)(Areas) 57 | : children} 58 | 59 | ) 60 | }, 61 | ) 62 | 63 | Composition.displayName = 'Composition' 64 | 65 | export default Composition 66 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/components/Only.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { BoxProps } from '@atomic-layout/core' 3 | import Box from './Box' 4 | import useResponsiveQuery, { 5 | ResponsiveQueryParams, 6 | } from '../hooks/useResponsiveQuery' 7 | import { forwardRef } from '../utils/forwardRef' 8 | 9 | export type OnlyProps = BoxProps & ResponsiveQueryParams 10 | 11 | const Only = forwardRef( 12 | ({ children, except, for: exactBreakpoint, from, to, ...restProps }, ref) => { 13 | const matches = useResponsiveQuery({ 14 | for: exactBreakpoint, 15 | from, 16 | to, 17 | except, 18 | }) 19 | 20 | return ( 21 | matches && ( 22 | 23 | {children} 24 | 25 | ) 26 | ) 27 | }, 28 | ) 29 | 30 | Only.displayName = 'Only' 31 | 32 | export default Only 33 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/components/Visible.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import Box from './Box' 4 | import { OnlyProps } from './Only' 5 | import useResponsiveQuery from '../hooks/useResponsiveQuery' 6 | import { forwardRef } from '../utils/forwardRef' 7 | 8 | const VisibleContainer = styled(Box)<{ matches: boolean }>` 9 | ${({ matches }) => 10 | !matches && 11 | ` 12 | visibility: hidden; 13 | `} 14 | ` 15 | 16 | const Visible = forwardRef( 17 | ( 18 | { children, except, for: exactBreakpointName, from, to, ...boxProps }, 19 | ref, 20 | ) => { 21 | const matches = useResponsiveQuery({ 22 | except, 23 | for: exactBreakpointName, 24 | from, 25 | to, 26 | }) 27 | const ariaAttributes = !matches ? { 'aria-hidden': 'true' } : {} 28 | 29 | return ( 30 | 36 | {children} 37 | 38 | ) 39 | }, 40 | ) 41 | 42 | Visible.displayName = 'Visible' 43 | 44 | export default Visible 45 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/hooks/useBreakpointChange.ts: -------------------------------------------------------------------------------- 1 | import { Layout, Breakpoints, createMediaQuery } from '@atomic-layout/core' 2 | import useViewportChange from './useViewportChange' 3 | 4 | /** 5 | * Executes a given callback upon any breakpoint change. 6 | * Callback calls are throttled by default. 7 | */ 8 | const useBreakpointChange = ( 9 | callback: (breakpointName: string) => void, 10 | throttleInterval?: number, 11 | breakpoints: Breakpoints = Layout.breakpoints, 12 | ) => { 13 | let prevBreakpointName: string 14 | 15 | useViewportChange(() => { 16 | const nextBreakpointName = Object.keys(breakpoints).find( 17 | (breakpointName) => { 18 | const mediaQuery = createMediaQuery(breakpoints[breakpointName], 'only') 19 | return matchMedia(mediaQuery).matches 20 | }, 21 | ) 22 | 23 | // Executes the callback only when breakpoint name has changed 24 | // between viewport changes. 25 | if (prevBreakpointName !== nextBreakpointName) { 26 | callback(nextBreakpointName) 27 | prevBreakpointName = nextBreakpointName 28 | } 29 | }, throttleInterval) 30 | } 31 | 32 | export default useBreakpointChange 33 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/hooks/useMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { useState, useMemo, useEffect, useLayoutEffect } from 'react' 2 | import { 3 | MediaQuery as MediaQueryParams, 4 | compose, 5 | joinQueryList, 6 | normalizeQuery, 7 | transformNumeric, 8 | } from '@atomic-layout/core' 9 | 10 | /** 11 | * Creates a media querty string based on the given params. 12 | */ 13 | export const createMediaQuery = (queryParams: MediaQueryParams): string => { 14 | return compose( 15 | joinQueryList(([paramName, paramValue]) => { 16 | /** 17 | * Transform values that begin with a number to prevent 18 | * transformations of "calc" expressions. 19 | * Transformation of numerics is necessary when a simple 20 | * number is used as a value (min-width: 750) is not valid. 21 | * 22 | * (min-width: 750) ==> (min-width: 750px) 23 | */ 24 | const resolvedParamValue = /^\d/.test(String(paramValue)) 25 | ? transformNumeric(paramValue) 26 | : paramValue 27 | 28 | return `(${paramName}:${resolvedParamValue})` 29 | }), 30 | normalizeQuery, 31 | )(queryParams) 32 | } 33 | 34 | type UseMediaQuery = ( 35 | queryParams: MediaQueryParams[] | MediaQueryParams, 36 | initialMatches?: boolean, 37 | ) => boolean 38 | 39 | export const useMediaQuery: UseMediaQuery = ( 40 | queryParams, 41 | initialMatches = false, 42 | ): boolean => { 43 | const useSafeEffect = 44 | typeof window === 'undefined' ? useEffect : useLayoutEffect 45 | const [matches, setMatches] = useState(initialMatches) 46 | const query = useMemo(() => { 47 | return [] 48 | .concat(queryParams) 49 | .map(createMediaQuery) 50 | .join(',') 51 | }, [queryParams]) 52 | 53 | const handleMediaQueryChange = ( 54 | mediaQueryList: MediaQueryList | MediaQueryListEvent, 55 | ) => { 56 | setMatches(mediaQueryList.matches) 57 | } 58 | 59 | useSafeEffect(() => { 60 | const mediaQueryList = matchMedia(query) 61 | handleMediaQueryChange(mediaQueryList) 62 | mediaQueryList.addListener(handleMediaQueryChange) 63 | 64 | return () => { 65 | mediaQueryList.removeListener(handleMediaQueryChange) 66 | } 67 | }, Object.keys(queryParams)) 68 | 69 | return matches 70 | } 71 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/hooks/useResponsiveQuery.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { Breakpoint } from '@atomic-layout/core' 3 | import useViewportChange from './useViewportChange' 4 | import { query } from '../utils/query' 5 | 6 | export type BreakpointRef = Breakpoint | string 7 | 8 | export interface ResponsiveQueryParams { 9 | /** 10 | * Renders children only at the specified breakpoint. 11 | */ 12 | for?: BreakpointRef 13 | /** 14 | * Renders children from the specified breakpoint and up, 15 | * unless enclosing `to` prop is set to form a range. 16 | */ 17 | from?: BreakpointRef 18 | /** 19 | * Renders children from the specified breakpoint and down, 20 | * unless the openning `from` prop is set to form a range. 21 | */ 22 | to?: BreakpointRef 23 | /** 24 | * Renders children everywhere except the given breakpoint range. 25 | */ 26 | except?: boolean 27 | } 28 | 29 | /** 30 | * Returns a boolean indicating that the current viewport matches the given responsive query. 31 | * @example 32 | * const matches = useResponsiveQuery({ from: 'sm', to: 'lg' }) 33 | * const matches = useResponsiveQuery({ except: true, from: 'md', to: 'lg' }) 34 | */ 35 | export default function useResponsiveQuery( 36 | params: ResponsiveQueryParams, 37 | initialMatches: boolean = false, 38 | ): boolean { 39 | const [matches, setMatches] = useState(initialMatches) 40 | const mediaQuery = query(params) 41 | 42 | useViewportChange(() => { 43 | const { matches: hasMatchingQuery } = matchMedia(mediaQuery) 44 | setMatches(hasMatchingQuery) 45 | }) 46 | 47 | return matches 48 | } 49 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/hooks/useResponsiveValue.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { withBreakpoints } from '@atomic-layout/core' 3 | import useBreakpointChange from './useBreakpointChange' 4 | 5 | /** 6 | * Accepts a map of { breakpointName: value } pairs 7 | * and returns a value based on the current viewport. 8 | * Returns default value when no matching pair is found. 9 | */ 10 | const useResponsiveValue = ( 11 | breakpoints: Record, 12 | defaultValue?: ValueType, 13 | ): ValueType => { 14 | const [value, setValue] = useState(defaultValue) 15 | 16 | const callback = () => { 17 | const nextValue = withBreakpoints(breakpoints, defaultValue) 18 | setValue(nextValue) 19 | } 20 | 21 | useEffect(callback, [breakpoints, defaultValue]) 22 | 23 | useBreakpointChange(callback) 24 | 25 | return value 26 | } 27 | 28 | export default useResponsiveValue 29 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/hooks/useViewportChange.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | import { throttle } from '@atomic-layout/core' 3 | 4 | /** 5 | * Executes a callback on viewport change (window resize). 6 | * Callback calls are throttled by default. 7 | */ 8 | const useViewportChange = ( 9 | callback: () => void, 10 | throttleInterval: number = 70, 11 | ) => { 12 | const handleWindowResize = useRef<(...args: any[]) => any>() 13 | 14 | useEffect(() => { 15 | handleWindowResize.current = throttle(callback, throttleInterval) 16 | }) 17 | 18 | useEffect(() => { 19 | const { current } = handleWindowResize 20 | 21 | current() 22 | window.addEventListener('resize', current) 23 | return () => window.removeEventListener('resize', current) 24 | }, []) 25 | } 26 | 27 | export default useViewportChange 28 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Layout } from '@atomic-layout/core' 2 | export default Layout 3 | 4 | /* Components */ 5 | export { default as Box } from './components/Box' 6 | export { default as Composition } from './components/Composition' 7 | export { default as Only } from './components/Only' 8 | export { default as Visible } from './components/Visible' 9 | 10 | /* Hooks */ 11 | export { useMediaQuery } from './hooks/useMediaQuery' 12 | export { default as useViewportChange } from './hooks/useViewportChange' 13 | export { default as useBreakpointChange } from './hooks/useBreakpointChange' 14 | export { default as useResponsiveValue } from './hooks/useResponsiveValue' 15 | export { default as useResponsiveProps } from './hooks/useResponsiveProps' 16 | export { default as useResponsiveQuery } from './hooks/useResponsiveQuery' 17 | 18 | /* Utils */ 19 | export { query } from './utils/query' 20 | export { makeResponsive } from './utils/makeResponsive' 21 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/utils/MakeResponsive.spec.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | import React from 'react' 5 | import { renderToString } from 'react-dom/server' 6 | import { makeResponsive } from './makeResponsive' 7 | 8 | const Component = makeResponsive((props) => { 9 | return 10 | }) 11 | 12 | describe('makeResponsive', () => { 13 | describe('given rendered on a server', () => { 14 | let html: ReturnType 15 | 16 | beforeAll(() => { 17 | html = renderToString( 18 | , 19 | ) 20 | }) 21 | 22 | it('should have responsive prop with default breakpoint', () => { 23 | expect(html).toContain('src="image.png"') 24 | }) 25 | 26 | it.skip('should have responsive prop with "down" behavior', () => { 27 | expect(html).toContain('title="Title"') 28 | }) 29 | 30 | it('should not have any responsive prop with other breakpoints', () => { 31 | expect(html).not.toContain('alt') 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/utils/forwardRef.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export const forwardRef = ( 4 | component: React.RefForwardingComponent, 5 | ): React.FC => { 6 | return React.forwardRef(component) 7 | } 8 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/utils/getBreakpointsByQuery.test.ts: -------------------------------------------------------------------------------- 1 | import { getBreakpointsByQuery } from './getBreakpointsByQuery' 2 | 3 | describe('getBreakpointsByQuery', () => { 4 | describe('given an exact breakpoint (for)', () => { 5 | let result: ReturnType 6 | 7 | beforeAll(() => { 8 | result = getBreakpointsByQuery({ for: 'md' }) 9 | }) 10 | 11 | it('should return a single enclosed breakpoint', () => { 12 | expect(result).toEqual([ 13 | { 14 | minWidth: '768px', 15 | maxWidth: '991px', 16 | }, 17 | ]) 18 | }) 19 | }) 20 | 21 | describe('given a high-pass breakpoint range (from)', () => { 22 | let result: ReturnType 23 | 24 | beforeAll(() => { 25 | result = getBreakpointsByQuery({ from: 'sm' }) 26 | }) 27 | 28 | it('should return breakpoints for that high-pass range', () => { 29 | expect(result).toEqual([ 30 | { 31 | minWidth: '576px', 32 | }, 33 | ]) 34 | }) 35 | }) 36 | 37 | describe('given a low-pass breakpoint range (to)', () => { 38 | let result: ReturnType 39 | 40 | beforeAll(() => { 41 | result = getBreakpointsByQuery({ to: 'md' }) 42 | }) 43 | 44 | it('should return breakpoints for that low-pass range', () => { 45 | expect(result).toEqual([ 46 | { 47 | maxWidth: '767px', 48 | }, 49 | ]) 50 | }) 51 | }) 52 | 53 | describe.only('given a bell breakpoint range (from/to)', () => { 54 | let result: ReturnType 55 | 56 | beforeAll(() => { 57 | result = getBreakpointsByQuery({ from: 'sm', to: 'lg' }) 58 | }) 59 | 60 | it('should return breakpoints for that inclusive range', () => { 61 | expect(result).toEqual([ 62 | { 63 | minWidth: '576px', 64 | maxWidth: 'calc(992px - 1px)', 65 | }, 66 | ]) 67 | }) 68 | }) 69 | 70 | describe('given a notch breakpoint range (except/from/to)', () => { 71 | let result: ReturnType 72 | 73 | beforeAll(() => { 74 | result = getBreakpointsByQuery({ except: true, from: 'sm', to: 'lg' }) 75 | }) 76 | 77 | it('should return breakpoints for that exclusive range', () => { 78 | expect(result).toEqual([ 79 | { 80 | maxWidth: '575px', 81 | }, 82 | { 83 | maxWidth: undefined, 84 | minWidth: '992px', 85 | }, 86 | ]) 87 | }) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/utils/getBreakpointsByQuery.ts: -------------------------------------------------------------------------------- 1 | import { 2 | mergeAreaRecords, 3 | Breakpoint, 4 | Layout, 5 | openBreakpoint, 6 | closeBreakpoint, 7 | } from '@atomic-layout/core' 8 | import { 9 | ResponsiveQueryParams, 10 | BreakpointRef, 11 | } from '../hooks/useResponsiveQuery' 12 | 13 | export const resolveBreakpoint = (breakpointRef: BreakpointRef): Breakpoint => { 14 | return typeof breakpointRef === 'string' 15 | ? Layout.breakpoints[breakpointRef] 16 | : breakpointRef 17 | } 18 | 19 | /** 20 | * Returns a list of breakpoints based on a responsive query. 21 | * @example 22 | * getBreakpointsByQuery({ from: 'md' }) 23 | * // [{ minWidth: 768 }] 24 | * getBreakpointsByQuery({ from: 'sm', to: 'lg' }) 25 | * // [{ minWidth: 576 }, { maxWidth: 1199 }] 26 | */ 27 | export const getBreakpointsByQuery = ( 28 | params: ResponsiveQueryParams, 29 | ): Breakpoint[] => { 30 | const { for: exactBreakpoint, from, to, except } = params 31 | 32 | // Explicit breakpoint 33 | if (exactBreakpoint) { 34 | return [resolveBreakpoint(exactBreakpoint)] 35 | } 36 | 37 | const minBreakpoint = resolveBreakpoint(from) 38 | const maxBreakpoint = resolveBreakpoint(to) 39 | 40 | // Bell, __/--\__ 41 | if (minBreakpoint && maxBreakpoint && !except) { 42 | const mergedAreaRecord = mergeAreaRecords( 43 | { 44 | behavior: 'down', 45 | breakpoint: maxBreakpoint, 46 | }, 47 | { 48 | behavior: 'up', 49 | breakpoint: minBreakpoint, 50 | }, 51 | false, 52 | ) 53 | 54 | return [mergedAreaRecord.breakpoint] 55 | } 56 | 57 | // Notch, --\__/-- 58 | if (minBreakpoint && maxBreakpoint && except) { 59 | return [closeBreakpoint(minBreakpoint), openBreakpoint(maxBreakpoint)] 60 | } 61 | 62 | // High-pass, __/-- 63 | if (minBreakpoint && !maxBreakpoint) { 64 | return [openBreakpoint(minBreakpoint)] 65 | } 66 | 67 | // Low-pass, --\__ 68 | if (!minBreakpoint && maxBreakpoint) { 69 | return [closeBreakpoint(maxBreakpoint)] 70 | } 71 | 72 | return [] 73 | } 74 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/utils/makeResponsive.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Numeric } from '@atomic-layout/core' 3 | import useResponsiveProps from '../hooks/useResponsiveProps' 4 | 5 | /** 6 | * Returns a new React component based on the given one 7 | * that enables support for Responsive Props API on arbitrary props. 8 | */ 9 | export function makeResponsive< 10 | OwnProps extends Record, 11 | ResponsiveProps extends Record, 12 | RefType = unknown 13 | >( 14 | Component: React.FC, 15 | ): React.FC>> { 16 | return React.forwardRef>( 17 | (responsiveProps, ref) => { 18 | /** 19 | * @see https://github.com/Microsoft/TypeScript/issues/29049 20 | */ 21 | const actualProps = useResponsiveProps( 22 | responsiveProps, 23 | ) as OwnProps & Partial 24 | 25 | return 26 | }, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/utils/query.test.ts: -------------------------------------------------------------------------------- 1 | import { query } from './query' 2 | 3 | describe('query', () => { 4 | describe('given an exact breakpoint (for)', () => { 5 | let result: ReturnType 6 | 7 | beforeAll(() => { 8 | result = query({ for: 'md' }) 9 | }) 10 | 11 | it('should return an enclosed media query for the given breakpoint', () => { 12 | expect(result).toEqual('(min-width:768px) and (max-width:991px)') 13 | }) 14 | }) 15 | 16 | describe('given a high-pass breakpoint range (from)', () => { 17 | let result: ReturnType 18 | 19 | beforeAll(() => { 20 | result = query({ from: 'md' }) 21 | }) 22 | 23 | it('should return an enclosed media query for the given range', () => { 24 | expect(result).toEqual('(min-width:768px)') 25 | }) 26 | }) 27 | 28 | describe('given a low-pass breakpoint range (to)', () => { 29 | let result: ReturnType 30 | 31 | beforeAll(() => { 32 | result = query({ to: 'lg' }) 33 | }) 34 | 35 | it('should return an enclosed media query for the given range', () => { 36 | expect(result).toEqual('(max-width:991px)') 37 | }) 38 | }) 39 | 40 | describe('given a bell breakpoint range (from/to)', () => { 41 | let result: ReturnType 42 | 43 | beforeAll(() => { 44 | result = query({ from: 'sm', to: 'lg' }) 45 | }) 46 | 47 | it('should return an enclosed media query for the given range', () => { 48 | expect(result).toEqual( 49 | '(min-width:576px) and (max-width:calc(992px - 1px))', 50 | ) 51 | }) 52 | }) 53 | 54 | describe('given a notch breakpoint range (except/from/to)', () => { 55 | let result: ReturnType 56 | 57 | beforeAll(() => { 58 | result = query({ except: true, from: 'sm', to: 'lg' }) 59 | }) 60 | 61 | it('should return an enclosed media query for the given range', () => { 62 | expect(result).toEqual('(max-width:575px),(min-width:992px)') 63 | }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/utils/query.ts: -------------------------------------------------------------------------------- 1 | import { memoizeWith } from '@atomic-layout/core' 2 | import { createMediaQuery } from '../hooks/useMediaQuery' 3 | import { ResponsiveQueryParams } from '../hooks/useResponsiveQuery' 4 | import { getBreakpointsByQuery } from './getBreakpointsByQuery' 5 | 6 | const createQuery = (params: ResponsiveQueryParams): string => { 7 | const breakpoints = getBreakpointsByQuery(params) 8 | return breakpoints.map(createMediaQuery).join(params.except ? ',' : ' ') 9 | } 10 | 11 | /** 12 | * Converts a responsive query into a @media query string. 13 | * @example 14 | * query({ from: 'md' }) 15 | * // (min-width: 768px) 16 | * query({ from: 'sm', to: 'lg' }) 17 | * // (min-width: 576px) and (max-width: 1199px) 18 | * query({ for: 'md' }) 19 | * // (min-width: 768px) and (max-width: 991px) 20 | * query({ except: true, from: 'sm', to: 'lg' }) 21 | * // (max-width: 575px), (min-width: 992px) 22 | */ 23 | export const query = memoizeWith((params) => { 24 | return Object.entries(params) 25 | .filter(([, value]) => value != null) 26 | .reduce((acc, [key, value]) => { 27 | return acc.concat(`${key}=${value.toString()}`) 28 | }, []) 29 | .join() 30 | })(createQuery) 31 | -------------------------------------------------------------------------------- /packages/atomic-layout/src/utils/withPlaceholder.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Breakpoint, AreaComponent, GenericProps } from '@atomic-layout/core' 3 | import { forwardRef } from './forwardRef' 4 | import { useMediaQuery } from '../hooks/useMediaQuery' 5 | 6 | /** 7 | * Wraps the given area component in a placeholder component. 8 | * This is used for conditional components, where placeholder component is rendered 9 | * until the condition for that area component is met (i.e. viewport matches a breakpoint). 10 | */ 11 | export const withPlaceholder = ( 12 | Component: AreaComponent, 13 | breakpoints: Breakpoint[], 14 | ) => { 15 | const Placeholder = forwardRef( 16 | ({ children, ...restProps }, ref) => { 17 | const matches = useMediaQuery(breakpoints) 18 | return ( 19 | matches && ( 20 | 21 | {children} 22 | 23 | ) 24 | ) 25 | }, 26 | ) 27 | 28 | Placeholder.displayName = `Placeholder(${Component.displayName})` 29 | 30 | return Placeholder 31 | } 32 | -------------------------------------------------------------------------------- /packages/atomic-layout/test/createForwardRefTest.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from '@testing-library/react' 3 | import '../../atomic-layout-core/src/utils/breakpoints/withBreakpoints/matchMedia.mock' 4 | import { 5 | Box as DefaultBox, 6 | Composition as DefaultComposition, 7 | Only as DefaultOnly, 8 | Visible as DefaultVisible, 9 | } from '../src/' 10 | 11 | interface ComponentsMap { 12 | Box: typeof DefaultBox 13 | Composition: typeof DefaultComposition 14 | Only: typeof DefaultOnly 15 | Visible: typeof DefaultVisible 16 | } 17 | 18 | export const createForwardRefTest = (components: ComponentsMap) => { 19 | const { Box, Composition, Only, Visible } = components 20 | 21 | describe('Refs', () => { 22 | it('Supports ref forwarding for the "Box" component', () => { 23 | const ref = React.createRef() 24 | render() 25 | expect(ref.current).toBeInstanceOf(HTMLDivElement) 26 | }) 27 | 28 | it('Supports ref forwarding for the "Composition" component', () => { 29 | const ref = React.createRef() 30 | render( 31 | 32 |

Arbitrary content

33 |
, 34 | ) 35 | expect(ref.current).toBeInstanceOf(HTMLSpanElement) 36 | }) 37 | 38 | it('Supports ref forwarding for the generated Area components', () => { 39 | const leftRef = React.createRef() 40 | const rightRef = React.createRef() 41 | render( 42 | 43 | {(Areas) => ( 44 | <> 45 | Left 46 | 47 | Right 48 | 49 | 50 | )} 51 | , 52 | ) 53 | expect(leftRef.current).toBeInstanceOf(HTMLDivElement) 54 | expect(rightRef.current).toBeInstanceOf(HTMLSpanElement) 55 | }) 56 | 57 | it('Supports ref forwarding for the "Only" component', () => { 58 | const ref = React.createRef() 59 | render() 60 | expect(ref.current).toBeInstanceOf(HTMLDivElement) 61 | }) 62 | 63 | it('Supports ref forwarding for the "Visible" component', () => { 64 | const ref = React.createRef() 65 | render() 66 | expect(ref.current).toBeInstanceOf(HTMLSpanElement) 67 | }) 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /packages/atomic-layout/test/createSsrTest.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { renderToString } from 'react-dom/server' 3 | 4 | /** 5 | * Creates a SSR unit test scenario with the given built module. 6 | */ 7 | export const createSsrTest = ( 8 | getModule: () => Promise, 9 | ): void => { 10 | describe('Server-side rendering', () => { 11 | let library: typeof import('../src/index') 12 | 13 | beforeAll(async () => { 14 | library = await getModule() 15 | }) 16 | 17 | it('rendering on a server without crashing', () => { 18 | const { Box, Only, Composition } = library 19 | 20 | const renderOnServer = () => 21 | renderToString( 22 |
23 | 24 | Responsive content 25 | 26 | {({ First, Second }: any) => ( 27 | <> 28 | First 29 | Second 30 | 31 | )} 32 | 33 |
, 34 | ) 35 | 36 | expect(renderOnServer).not.toThrow() 37 | }) 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /packages/atomic-layout/test/propAliases.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { propAliases } from '@atomic-layout/core' 3 | import Composition from '../src/components/Composition' 4 | import { render, cleanup, getByTestId } from '@testing-library/react' 5 | import '@testing-library/jest-dom/extend-expect' 6 | 7 | const defaultValue = 10 8 | const explicitValues: Record< 9 | string, 10 | string | number | Array 11 | > = { 12 | area: 'first', 13 | // prettier-ignore 14 | areas: 'first', 15 | // prettier-ignore 16 | template: "first", 17 | templateCols: '500px', 18 | templateRows: '500px', 19 | col: '1 / auto', 20 | colStart: '2', 21 | colEnd: '3', 22 | gap: '20px 25px', 23 | gutter: '20px 25px', 24 | margin: ['0px', 10], 25 | flexDirection: ['row', 'column'], 26 | flexShrink: ['0', '1'], 27 | flexGrow: ['1', '0'], 28 | flexWrap: ['nowrap', 'wrap', 'wrap-reverse'], 29 | row: '1 / auto', 30 | rowStart: '2', 31 | rowEnd: '3', 32 | autoFlow: ['row', 'column', 'row dense'], 33 | align: 'center', 34 | alignItems: 'flex-end', 35 | alignContent: ['flex-start', 'space-around', 'space-between'], 36 | justify: 'center', 37 | justifyItems: 'flex-end', 38 | justifyContent: 'flex-start', 39 | place: 'center center', 40 | placeItems: 'flex-end flex-end', 41 | placeContent: 'flex-start flex-start', 42 | } 43 | 44 | describe('Prop aliases', () => { 45 | afterEach(cleanup) 46 | 47 | Object.keys(propAliases).forEach((propAliasName) => { 48 | describe(propAliasName, () => { 49 | const propValues: Array = [].concat( 50 | explicitValues[propAliasName] || defaultValue, 51 | ) 52 | 53 | propValues.forEach((propValue) => { 54 | describe(`given the value: ${propValue}`, () => { 55 | const props = { 56 | [propAliasName]: propValue, 57 | } 58 | const { container } = render( 59 | 60 | {({ First }) => {propAliasName}} 61 | , 62 | ) 63 | const element = getByTestId(container, 'composition') 64 | 65 | const { props: cssProps, transformValue } = propAliases[propAliasName] 66 | const expectedValue = transformValue 67 | ? transformValue(propValue) 68 | : propValue 69 | 70 | cssProps.forEach((cssPropName) => { 71 | const expectedStylesString = `${cssPropName}:${expectedValue}` 72 | 73 | it(`produces "${expectedStylesString}"`, () => { 74 | expect(element).toHaveStyle(expectedStylesString) 75 | }) 76 | }) 77 | }) 78 | }) 79 | }) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /packages/atomic-layout/test/refs.spec.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Composition, Only, Visible } from '../src' 2 | import { createForwardRefTest } from './createForwardRefTest' 3 | 4 | createForwardRefTest({ 5 | Box, 6 | Composition, 7 | Only, 8 | Visible, 9 | }) 10 | -------------------------------------------------------------------------------- /packages/atomic-layout/test/ssr.spec.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | import { createSsrTest } from './createSsrTest' 5 | 6 | createSsrTest(() => import('../src')) 7 | -------------------------------------------------------------------------------- /packages/atomic-layout/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "module": "es6", 5 | "target": "esnext", 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "emitDeclarationOnly": true, 9 | "declarationDir": "lib/types", 10 | "noImplicitThis": false, 11 | "noImplicitAny": true, 12 | "esModuleInterop": true, 13 | "jsx": "react", 14 | "baseUrl": ".", 15 | "types": ["jest"] 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "**/*.spec.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/atomic-layout/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-react", "../../tslint.json"], 3 | "rules": { 4 | "no-console": true, 5 | "no-submodule-imports": false, 6 | "jsx-boolean-value": ["never"], 7 | "jsx-no-multiline-js": false, 8 | "jsx-wrap-multiline": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-config-prettier"], 3 | "rules": { 4 | "no-console": false, 5 | "prefer-template": [true, "always"], 6 | "no-bitwise": false, 7 | 8 | "no-submodule-imports": [true, "@testing-library/jest-dom/extend-expect"], 9 | "no-implicit-dependencies": [true, "dev"], 10 | "ordered-imports": [false, "never"], 11 | "object-literal-sort-keys": [false, "never"], 12 | "only-arrow-functions": [false], 13 | "interface-name": [true, "never-prefix"], 14 | "no-object-literal-type-assertion": false 15 | } 16 | } 17 | --------------------------------------------------------------------------------