├── .coveralls.yml ├── .eslintrc ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.json ├── package.json ├── setupTests.js ├── src ├── docs │ ├── App.d.ts │ ├── App.tsx │ ├── Box.tsx │ ├── Form │ │ ├── Input.tsx │ │ ├── Label.tsx │ │ ├── Select.tsx │ │ ├── TextArea.tsx │ │ └── index.tsx │ ├── Text.tsx │ ├── index.html │ └── styles │ │ ├── animations.ts │ │ ├── breakpoints.ts │ │ ├── colors.ts │ │ ├── components │ │ └── buttonlike.ts │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── shadows.ts │ │ ├── theme.ts │ │ └── typography.ts └── lib │ ├── ScheduleSelector.tsx │ ├── colors.ts │ ├── date-utils.ts │ ├── index.ts │ ├── selection-schemes │ ├── index.d.ts │ ├── index.ts │ ├── linear.d.ts │ ├── linear.ts │ ├── square.d.ts │ └── square.ts │ └── typography.ts ├── test └── lib │ ├── ScheduleSelector.test.tsx │ ├── __snapshots__ │ └── ScheduleSelector.test.tsx.snap │ ├── date-utils.test.ts │ └── selection-schemes │ ├── linear.test.ts │ └── square.test.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: ivG86Ugx7tvknfKMh77V3L72WFpR1b2Hu -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "plugin:prettier/recommended", 5 | "plugin:import/errors", 6 | "plugin:import/warnings", 7 | "plugin:import/typescript", 8 | "plugin:import/extensions" 9 | ], 10 | "plugins": [], 11 | "parser": "babel-eslint", 12 | "parserOptions": { 13 | "ecmaVersion": 2016, 14 | "sourceType": "module", 15 | "ecmaFeatures": { 16 | "jsx": true 17 | } 18 | }, 19 | "settings": { 20 | "import/extensions": [ 21 | "error", 22 | "ignorePackages", 23 | { 24 | "js": "never", 25 | "jsx": "never", 26 | "ts": "never", 27 | "tsx": "never" 28 | } 29 | ] 30 | }, 31 | "env": { 32 | "es6": true, 33 | "browser": true, 34 | "node": true, 35 | "jest": true 36 | }, 37 | "rules": { 38 | "import/prefer-default-export": 0, 39 | "react/sort-comp": 0, 40 | "class-methods-use-this": 1, 41 | "react/require-default-props": 0, 42 | "react/prop-types": 0, 43 | "react/no-unused-prop-types": 0, 44 | "no-else-return": 0, 45 | "no-param-reassign": 0, 46 | "react/jsx-filename-extension": 0 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | yarn-error.log 4 | dev 5 | coverage 6 | dist 7 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | cache: 5 | directories: 6 | - node_modules 7 | install: 8 | - yarn 9 | script: 10 | - yarn cover -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest Current File", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": ["${fileBasenameNoExtension}"], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "disableOptimisticBPs": true, 16 | "windows": { 17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.validate.enable": false, 3 | "editor.wordWrap": "off" 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v3.0 4 | 5 | - component internally redesigned to use CSS Grid rather than Flexbox, resulting in cleaner markup and CSS 6 | - component internally redesigned to use Typescript rather than Flow, package now includes TS type delcarations 7 | - removes the `margin` prop in favor of `rowGap` and `columnGap` props 8 | - adds `renderTimeLabel` and `renderDateLabel` render props for greater customizability of the labels 9 | - component properly reacts to prop updates (previously changing certain props that control how many cells show up wouldn't trigger a refresh) 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bibek Ghimire 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Schedule Selector 2 | 3 | [![npm version](https://badge.fury.io/js/react-schedule-selector.svg)](https://badge.fury.io/js/react-schedule-selector) [![Build Status](https://travis-ci.com/bibekg/react-schedule-selector.svg?branch=master)](https://travis-ci.com/bibekg/react-schedule-selector) [![Coverage Status](https://coveralls.io/repos/github/bibekg/react-schedule-selector/badge.svg?branch=master)](https://coveralls.io/github/bibekg/react-schedule-selector?branch=master) 4 | 5 | A mobile-friendly when2meet-style grid-based schedule selector built with [styled components](https://github.com/styled-components/styled-components) and [date-fns](https://date-fns.org/). 6 | 7 | [Live example](http://react-schedule-selector.surge.sh/) 8 | 9 | ![image](https://image.ibb.co/jDKJBT/react_grid_date_picker.png) 10 | 11 | ## Getting Started 12 | 13 | ``` 14 | yarn add react-schedule-selector styled-components 15 | ``` 16 | 17 | ```js 18 | import ScheduleSelector from 'react-schedule-selector' 19 | 20 | class App extends React.Component { 21 | state = { schedule = [] } 22 | 23 | handleChange = newSchedule => { 24 | this.setState({ schedule: newSchedule }) 25 | } 26 | 27 | render() { 28 | return ( 29 | 37 | ) 38 | } 39 | } 40 | ``` 41 | 42 | ## `` 43 | 44 | `ScheduleSelector` is a controlled component that can be used easily with the default settings. Just provide a controlled value for `selection` and include an `onChange` handler and you're good to go. Further customization can be done using the props outlined below. 45 | 46 | To customize the UI, you can either: 47 | 48 | 1. Specify values for the color, margin, format, etc. props 49 | 2. Use the `renderDateCell` render prop to handle rendering yourself. 50 | 51 | ### `Props` 52 | 53 | #### `selection` 54 | 55 | **type**: `Array` 56 | 57 | **description**: List of dates that should be filled in on the grid (reflect the start time of each cell). 58 | 59 | **required**: yes 60 | 61 | #### `selectionScheme` 62 | 63 | **type**: `'square'` | `'linear'` 64 | 65 | **description**: The behavior for selection when dragging. `square` selects a square with the start and end cells at opposite corners. `linear` selects all the cells that are chronologically between the start and end cells. 66 | 67 | **required**: no 68 | 69 | **default value**: `'square'` 70 | 71 | #### `onChange` 72 | 73 | **type**: `(Array) => void` 74 | 75 | **description**: Called when selected availability is changed. The new list of selected dates is passed in as the first parameter. 76 | 77 | **required**: yes 78 | 79 | #### `startDate` 80 | 81 | **type**: `Date` 82 | 83 | **description**: The date on which the grid should start (time portion is ignored, specify start time via `minTime`) 84 | 85 | **required**: no 86 | 87 | **default value**: today 88 | 89 | #### `numDays` 90 | 91 | **type**: `number` 92 | 93 | **description**: The number of days to show, starting from today 94 | 95 | **required**: no 96 | 97 | **default value**: `7` 98 | 99 | #### `hourlyChunks` 100 | 101 | **type**: `number` 102 | 103 | **description**: How many chunks to divide each hour into (e.g. `2` divides the hour into half-hour steps, `4` into 15-minute steps) 104 | 105 | **required**: no 106 | 107 | **default value**: `1` 108 | 109 | #### `minTime` 110 | 111 | **type**: `number` 112 | 113 | **description**: The minimum hour to show (0-23) 114 | 115 | **required**: no 116 | 117 | **default value**: `9` 118 | 119 | #### `maxTime` 120 | 121 | **type**: `number` 122 | 123 | **description**: The maximum hour to show (0-23) 124 | 125 | **required**: no 126 | 127 | **default value**: `23` 128 | 129 | #### `dateFormat` 130 | 131 | **type**: `string` 132 | 133 | **description**: The [date format](https://date-fns.org/v1.29.0/docs/format) to be used for the column headers 134 | 135 | **required**: no 136 | 137 | **default value**: `'M/D'` 138 | 139 | #### `timeFormat` 140 | 141 | **type**: `string` 142 | 143 | **description**: The [time format](https://date-fns.org/v1.29.0/docs/format) to be used for the row labels 144 | 145 | **required**: no 146 | 147 | **default value**: `'ha'` 148 | 149 | #### `margin` (removed in v3.0, use `columnGap` and `rowGap` instead) 150 | 151 | **type**: `number` 152 | 153 | **description**: The margin between grid cells (in pixels) 154 | 155 | **required**: no 156 | 157 | **default value**: `3` 158 | 159 | #### `columnGap` 160 | 161 | **type**: `string` 162 | 163 | **description**: The gap between grid columns, specified using any valid CSS units 164 | 165 | **required**: no 166 | 167 | **default value**: `'4 px'` 168 | 169 | #### `rowGap` 170 | 171 | **type**: `string` 172 | 173 | **description**: The gap between grid rows, specified using any valid CSS units 174 | 175 | **required**: no 176 | 177 | **default value**: `'4 px'` 178 | 179 | #### `unselectedColor` 180 | 181 | **type**: `string` 182 | 183 | **description**: The color of an unselected cell 184 | 185 | **required**: no 186 | 187 | **default value**: `'rgba(89, 154, 242, 1)'` 188 | 189 | #### `selectedColor` 190 | 191 | **type**: `string` 192 | 193 | **description**: The color of an unselected cell 194 | 195 | **required**: no 196 | 197 | **default value**: `'rgba(162, 198, 248, 1)'` 198 | 199 | #### `hoveredColor` 200 | 201 | **type**: `string` 202 | 203 | **description**: The color of a hovered cell 204 | 205 | **required**: no 206 | 207 | **default value**: `'#dbedff'` 208 | 209 | #### `renderDateCell` 210 | 211 | **type**: `(datetime: Date, selected: boolean, refSetter: (dateCell: HTMLElement | null) => void) => React.Node` 212 | 213 | **description**: A render prop function that accepts the time this cell is representing and whether the cell is selected or not and should return a React element. It is your responsibility to apply the `refSetter` as a ref to the component you render -- neglecting to do so will cause the component to not work properly for touch devices. If you choose to use this custom render function, the color props above have no effect. 214 | 215 | **required**: no 216 | 217 | #### `renderTimeLabel` 218 | 219 | **type**: `(time: Date) => React.Node` 220 | 221 | **description**: A render prop function that accepts the time a given row is representing and should return a React element. 222 | 223 | **required**: no 224 | 225 | #### `renderDateLabel` 226 | 227 | **type**: `(date: Date) => React.Node` 228 | 229 | **description**: A render prop function that accepts the time a given row is representing and should return a React element. 230 | 231 | **required**: no 232 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/react", 4 | "@babel/typescript", 5 | [ 6 | "@babel/env", 7 | { 8 | "targets": { 9 | "edge": "17", 10 | "firefox": "60", 11 | "chrome": "67", 12 | "safari": "11.1" 13 | }, 14 | "useBuiltIns": "usage", 15 | "corejs": "3.6.5" 16 | } 17 | ] 18 | ], 19 | "plugins": [ 20 | "transform-class-properties", 21 | "@babel/plugin-proposal-object-rest-spread", 22 | "@babel/plugin-proposal-nullish-coalescing-operator", 23 | [ 24 | "babel-plugin-styled-components", 25 | { 26 | "fileName": "false" 27 | } 28 | ] 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-schedule-selector", 3 | "version": "3.0.1", 4 | "description": "A mobile-friendly when2meet-style grid-based schedule selector", 5 | "author": "Bibek Ghimire", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/bibekg/react-schedule-selector.git" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "styled-components", 13 | "date", 14 | "grid" 15 | ], 16 | "main": "dist/lib/index.js", 17 | "types": "dist/lib/index.d.ts", 18 | "files": [ 19 | "dist/lib", 20 | "src/lib" 21 | ], 22 | "license": "MIT", 23 | "scripts": { 24 | "postpublish": "yarn docs:deploy", 25 | "build": "yarn clean && yarn lib:build && yarn lib:build-types && yarn docs:build", 26 | "lint": "eslint src/**/*.{js,jsx} --quiet", 27 | "format": "prettier --write \"src/**/*.{js,jsx}\"", 28 | "clean": "rm -rf dist", 29 | "cover": "jest --coverage && cat ./coverage/lcov.info | coveralls", 30 | "test": "TZ=UTC jest", 31 | "lib:build": "babel src/lib --out-dir dist/lib --extensions \".ts,.tsx\" --source-maps inline", 32 | "lib:build-types": "tsc --emitDeclarationOnly -d", 33 | "docs:dev": "parcel serve src/docs/index.html -d dev/docs", 34 | "docs:build": "parcel build src/docs/index.html -d dist/docs", 35 | "docs:deploy": "yarn docs:build && surge dist/docs --domain react-schedule-selector.surge.sh" 36 | }, 37 | "engines": { 38 | "node": ">8.0" 39 | }, 40 | "peerDependencies": { 41 | "react": ">=16.0", 42 | "styled-components": ">=5.0" 43 | }, 44 | "dependencies": { 45 | "@emotion/core": "^10.0.27", 46 | "@emotion/styled": "^10.0.27", 47 | "@types/jest": "^26.0.15", 48 | "@types/react-dom": "^18.0.11", 49 | "date-fns": "^1.29.0", 50 | "src": "^1.1.2" 51 | }, 52 | "devDependencies": { 53 | "@babel/cli": "^7.12.1", 54 | "@babel/core": "^7.12.3", 55 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", 56 | "@babel/plugin-proposal-object-rest-spread": "^7.12.1", 57 | "@babel/plugin-transform-typescript": "^7.12.1", 58 | "@babel/polyfill": "^7.12.1", 59 | "@babel/preset-env": "^7.12.1", 60 | "@babel/preset-react": "^7.12.1", 61 | "@babel/preset-typescript": "^7.12.1", 62 | "@types/enzyme": "^3.10.7", 63 | "@types/react": "^16.9.53", 64 | "@types/react-test-renderer": "^16.9.3", 65 | "@types/styled-components": "^5.1.4", 66 | "@types/styled-system": "^5.1.10", 67 | "@typescript-eslint/parser": "^5.53.0", 68 | "babel-eslint": "^8.2.3", 69 | "babel-jest": "^26.6.1", 70 | "babel-loader": "^7.1.4", 71 | "babel-plugin-styled-components": "^1.6.0", 72 | "babel-plugin-transform-class-properties": "^6.24.1", 73 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 74 | "babel-plugin-transform-react-jsx": "^6.24.1", 75 | "core-js": "^3.6.5", 76 | "coveralls": "^3.0.2", 77 | "enzyme": "^3.5.0", 78 | "enzyme-adapter-react-16": "^1.3.1", 79 | "eslint": "^4.19.1", 80 | "eslint-config-airbnb": "^16.1.0", 81 | "eslint-config-prettier": "^2.9.0", 82 | "eslint-loader": "^2.0.0", 83 | "eslint-plugin-import": "^2.11.0", 84 | "eslint-plugin-jsx-a11y": "^6.0.3", 85 | "eslint-plugin-prettier": "^2.6.0", 86 | "eslint-plugin-react": "^7.7.0", 87 | "jest": "^26.6.1", 88 | "jest-styled-components": "^7.0.3", 89 | "jsdom": "^12.0.0", 90 | "moment": "^2.22.2", 91 | "node-forge": "^0.10.0", 92 | "parcel-bundler": "^1.12.4", 93 | "prettier": "^1.14.2", 94 | "react": ">=16.3.0", 95 | "react-dom": "^16.3.2", 96 | "react-test-renderer": "^16.4.1", 97 | "regenerator-runtime": "^0.12.1", 98 | "styled-components": "^5.2.0", 99 | "styled-system": "^5.1.5", 100 | "surge": "^0.20.1", 101 | "typescript": "^4.0.3", 102 | "typescript-plugin-styled-components": "^1.4.4" 103 | }, 104 | "prettier": { 105 | "singleQuote": true, 106 | "semi": false, 107 | "printWidth": 120, 108 | "tabWidth": 2 109 | }, 110 | "jest": { 111 | "setupFilesAfterEnv": [ 112 | "/setupTests.js" 113 | ], 114 | "testMatch": [ 115 | "/**/*.{spec,test}.{js,jsx,ts,tsx}" 116 | ], 117 | "transform": { 118 | "^.+\\.(js|jsx|ts|tsx)$": "/node_modules/babel-jest" 119 | }, 120 | "verbose": false 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import Enzyme from 'enzyme' 3 | import EnzymeAdapter from 'enzyme-adapter-react-16' 4 | import 'jest-styled-components' 5 | 6 | process.env.TZ = 'UTC' 7 | 8 | // Setup enzyme's react adapter 9 | Enzyme.configure({ adapter: new EnzymeAdapter() }) 10 | -------------------------------------------------------------------------------- /src/docs/App.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /src/docs/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled, { createGlobalStyle } from 'styled-components' 3 | import * as ReactDOM from 'react-dom' 4 | import ScheduleSelector from '../lib' 5 | import { SelectionSchemeType } from '../lib/selection-schemes' 6 | import Box from './Box' 7 | import * as Form from './Form' 8 | import * as Text from './Text' 9 | 10 | const GlobalStyle = createGlobalStyle` 11 | body { 12 | font-family: sans-serif; 13 | } 14 | 15 | * { 16 | box-sizing: border-box; 17 | margin: 0; 18 | } 19 | ` 20 | 21 | const MainDiv = styled.div` 22 | padding-top: 40px; 23 | display: flex; 24 | flex-direction: column; 25 | align-items: center; 26 | ` 27 | 28 | const IntroText = styled.div` 29 | width: 100%; 30 | text-align: center; 31 | margin-bottom: 10px; 32 | ` 33 | 34 | const ScheduleSelectorCard = styled.div` 35 | border-radius: 25px; 36 | box-shadow: 10px 2px 30px rgba(0, 0, 0, 0.15); 37 | padding: 20px; 38 | width: 90%; 39 | max-width: 800px; 40 | & > * { 41 | flex-grow: 1; 42 | } 43 | ` 44 | 45 | const CustomizationRow = styled.div` 46 | display: flex; 47 | & > * { 48 | margin: 8px; 49 | } 50 | ` 51 | 52 | const Links = styled.div` 53 | display: flex; 54 | margin-top: 20px; 55 | ` 56 | 57 | const ExternalLink = styled.a` 58 | background-color: ${props => props.color}; 59 | color: white; 60 | padding: 10px; 61 | border-radius: 3px; 62 | cursor: pointer; 63 | text-decoration: none; 64 | margin: 5px; 65 | ` 66 | 67 | const App = () => { 68 | const [schedule, setSchedule] = React.useState>([]) 69 | const [selectionScheme, setSelectionScheme] = React.useState('linear') 70 | const [startDate, setStartDate] = React.useState(new Date()) 71 | const [numDays, setNumDays] = React.useState(7) 72 | const [hourlyChunks, setHourlyChunks] = React.useState(2) 73 | const [minTime, setMinTime] = React.useState(12) 74 | const [maxTime, setMaxTime] = React.useState(17) 75 | 76 | return ( 77 | 78 | 79 | 80 | React Schedule Selector 81 | Tap to select one time or drag to select multiple times at once. 82 | 83 | Customizable Props 84 | 85 | 86 | Selection scheme 87 | { 91 | if (event.target.value === 'linear' || event.target.value === 'square') { 92 | setSelectionScheme(event.target.value) 93 | } 94 | }} 95 | > 96 | 97 | 98 | 99 | 100 | 101 | 102 | Start date 103 | { 107 | const date = new Date(event.target.value) 108 | const tzOffset = date.getTimezoneOffset() 109 | setStartDate(new Date(+date + tzOffset * 60000)) 110 | }} 111 | /> 112 | 113 | 114 | 115 | Num days 116 | setNumDays(Number(event.target.value))} 122 | /> 123 | 124 | 125 | 126 | Min Time 127 | setMinTime(Number(event.target.value))} 133 | /> 134 | 135 | 136 | Max Time 137 | setMaxTime(Number(event.target.value))} 143 | /> 144 | 145 | 146 | Hourly chunks 147 | setHourlyChunks(Number(event.target.value))} 153 | /> 154 | 155 | 156 | 157 | 168 | 169 | 170 | 171 | GitHub 172 | 173 | 174 | NPM 175 | 176 | 177 | Medium 178 | 179 | 180 | 181 | ) 182 | } 183 | 184 | ReactDOM.render(, document.getElementById('app')) 185 | -------------------------------------------------------------------------------- /src/docs/Box.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { 3 | border, 4 | BorderProps, 5 | color, 6 | ColorProps, 7 | flexbox, 8 | FlexboxProps, 9 | grid, 10 | GridProps, 11 | layout, 12 | LayoutProps, 13 | position, 14 | PositionProps, 15 | ResponsiveValue, 16 | shadow, 17 | ShadowProps, 18 | space, 19 | SpaceProps, 20 | system, 21 | ThemeValue, 22 | background, 23 | BackgroundProps 24 | } from 'styled-system' 25 | 26 | type ContainerProp = { 27 | maxWidth: string 28 | minPadding: string 29 | } 30 | 31 | const isContainerPropBoolean = (cp: ContainerProp | Boolean): cp is Boolean => typeof cp === 'boolean' 32 | 33 | type BoxProps = { 34 | as?: string 35 | container?: ResponsiveValue> 36 | placeSelf?: ResponsiveValue> 37 | justifySelf?: ResponsiveValue> 38 | transform?: ResponsiveValue> 39 | cursor?: ResponsiveValue> 40 | textAlign?: ResponsiveValue> 41 | } & SpaceProps & 42 | PositionProps & 43 | ColorProps & 44 | BorderProps & 45 | LayoutProps & 46 | FlexboxProps & 47 | GridProps & 48 | ShadowProps & 49 | BackgroundProps 50 | 51 | const Box = styled.div( 52 | { 53 | boxSizing: 'border-box', 54 | minWidth: 0 55 | }, 56 | // Define a custom 'container' prop that lets us easily make a box a "container" which 57 | // offers a max-width and min-padding so that the box is resilient to extreme widths 58 | system({ 59 | container: { 60 | properties: ['paddingLeft', 'paddingRight'], 61 | transform: (value: ContainerProp | Boolean, scale) => { 62 | if (value) { 63 | const maxWidth = isContainerPropBoolean(value) ? '1280px' : value.maxWidth 64 | const minPadding = isContainerPropBoolean(value) ? '16px' : value.minPadding 65 | return `max(calc((100vw - ${maxWidth}) / 2), ${minPadding})` 66 | } 67 | } 68 | }, 69 | placeSelf: { 70 | property: 'placeSelf' 71 | }, 72 | justifySelf: { 73 | property: 'justifySelf' 74 | }, 75 | transform: { 76 | property: 'transform' 77 | }, 78 | cursor: { 79 | property: 'cursor' 80 | }, 81 | textAlign: { 82 | property: 'textAlign' 83 | } 84 | }), 85 | space, 86 | position, 87 | color, 88 | border, 89 | layout, 90 | flexbox, 91 | grid, 92 | shadow, 93 | background 94 | ) 95 | 96 | export default Box 97 | -------------------------------------------------------------------------------- /src/docs/Form/Input.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import theme from '../styles/theme' 3 | import { 4 | border, 5 | BorderProps, 6 | color, 7 | ColorProps, 8 | flexbox, 9 | FlexboxProps, 10 | fontSize, 11 | FontSizeProps, 12 | grid, 13 | GridProps, 14 | layout, 15 | LayoutProps, 16 | lineHeight, 17 | LineHeightProps, 18 | position, 19 | PositionProps, 20 | space, 21 | SpaceProps, 22 | typography, 23 | TypographyProps 24 | } from 'styled-system' 25 | 26 | type InputProps = { 27 | as?: string 28 | } & SpaceProps & 29 | PositionProps & 30 | ColorProps & 31 | BorderProps & 32 | LayoutProps & 33 | FlexboxProps & 34 | GridProps & 35 | FontSizeProps & 36 | LineHeightProps & 37 | TypographyProps 38 | 39 | const Input = styled.input( 40 | { 41 | borderRadius: '6px', 42 | padding: '10px 8px', 43 | border: `1px solid ${theme.colors.africanElephant}`, 44 | '&::placeholder': { 45 | color: theme.colors.africanElephant 46 | } 47 | }, 48 | space, 49 | position, 50 | color, 51 | border, 52 | layout, 53 | flexbox, 54 | grid, 55 | fontSize, 56 | lineHeight, 57 | typography 58 | ) 59 | 60 | Input.defaultProps = { 61 | ...theme.textStyles.input 62 | } 63 | 64 | export default Input 65 | -------------------------------------------------------------------------------- /src/docs/Form/Label.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { variant } from 'styled-system' 3 | 4 | import theme from '../styles/theme' 5 | import { LabelHTMLAttributes } from 'react' 6 | 7 | export interface LabelProps extends LabelHTMLAttributes { 8 | variant?: keyof typeof theme.textStyles | null 9 | } 10 | 11 | // @ts-ignore 12 | const Label = styled.label( 13 | { 14 | textTransform: 'uppercase', 15 | display: 'block' 16 | }, 17 | variant({ 18 | variants: theme.textStyles 19 | }) 20 | ) 21 | 22 | Label.defaultProps = { 23 | variant: 'label' 24 | } 25 | 26 | export default Label 27 | -------------------------------------------------------------------------------- /src/docs/Form/Select.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import theme from '../styles/theme' 3 | import { 4 | border, 5 | BorderProps, 6 | color, 7 | ColorProps, 8 | flexbox, 9 | FlexboxProps, 10 | fontSize, 11 | FontSizeProps, 12 | grid, 13 | GridProps, 14 | layout, 15 | LayoutProps, 16 | lineHeight, 17 | LineHeightProps, 18 | position, 19 | PositionProps, 20 | space, 21 | SpaceProps, 22 | typography, 23 | TypographyProps 24 | } from 'styled-system' 25 | 26 | type SelectProps = { 27 | as?: string 28 | } & SpaceProps & 29 | PositionProps & 30 | ColorProps & 31 | BorderProps & 32 | LayoutProps & 33 | FlexboxProps & 34 | GridProps & 35 | FontSizeProps & 36 | LineHeightProps & 37 | TypographyProps 38 | 39 | const Select = styled.select( 40 | { 41 | borderRadius: '6px', 42 | padding: '10px 8px', 43 | border: `1px solid ${theme.colors.africanElephant}`, 44 | '&::placeholder': { 45 | color: theme.colors.africanElephant 46 | } 47 | }, 48 | space, 49 | position, 50 | color, 51 | border, 52 | layout, 53 | flexbox, 54 | grid, 55 | fontSize, 56 | lineHeight, 57 | typography 58 | ) 59 | 60 | Select.defaultProps = { 61 | ...theme.textStyles.input 62 | } 63 | 64 | export default Select 65 | -------------------------------------------------------------------------------- /src/docs/Form/TextArea.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import theme from '../styles/theme' 3 | import { 4 | border, 5 | BorderProps, 6 | color, 7 | ColorProps, 8 | flexbox, 9 | FlexboxProps, 10 | fontSize, 11 | FontSizeProps, 12 | grid, 13 | GridProps, 14 | layout, 15 | LayoutProps, 16 | lineHeight, 17 | LineHeightProps, 18 | position, 19 | PositionProps, 20 | space, 21 | SpaceProps, 22 | typography, 23 | TypographyProps 24 | } from 'styled-system' 25 | 26 | type TextAreaProps = { 27 | as?: string 28 | } & SpaceProps & 29 | PositionProps & 30 | ColorProps & 31 | BorderProps & 32 | LayoutProps & 33 | FlexboxProps & 34 | GridProps & 35 | FontSizeProps & 36 | LineHeightProps & 37 | TypographyProps 38 | 39 | const TextArea = styled.textarea( 40 | { 41 | border: `1px solid ${theme.colors.africanElephant}`, 42 | borderRadius: '6px', 43 | padding: '12px', 44 | fontFamily: theme.fontFamilies.rubik, 45 | fontWeight: 400 46 | }, 47 | space, 48 | position, 49 | color, 50 | border, 51 | layout, 52 | flexbox, 53 | grid, 54 | fontSize, 55 | lineHeight, 56 | typography 57 | ) 58 | 59 | TextArea.defaultProps = { 60 | ...theme.textStyles.input, 61 | lineHeight: 1.5 62 | } 63 | 64 | export default TextArea 65 | -------------------------------------------------------------------------------- /src/docs/Form/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | interface FormProps {} 4 | 5 | export const Form = styled.form`` 6 | 7 | export default Form 8 | export { default as Input } from './Input' 9 | export { default as Label } from './Label' 10 | export { default as Select } from './Select' 11 | export { default as TextArea } from './TextArea' 12 | -------------------------------------------------------------------------------- /src/docs/Text.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import * as React from 'react' 3 | import theme from './styles/theme' 4 | import { baseTextStyles } from './styles/typography' 5 | import { 6 | color, 7 | ColorProps, 8 | typography, 9 | TypographyProps, 10 | lineHeight, 11 | LineHeightProps, 12 | ResponsiveValue, 13 | space, 14 | SpaceProps, 15 | grid, 16 | GridProps, 17 | system, 18 | ThemeValue, 19 | variant 20 | } from 'styled-system' 21 | 22 | export type TextProps = { 23 | variant?: keyof typeof theme.textStyles | null 24 | as?: string 25 | textAlign?: ResponsiveValue> 26 | } & TypographyProps & 27 | ColorProps & 28 | LineHeightProps & 29 | SpaceProps & 30 | GridProps 31 | 32 | const Text = styled.p( 33 | baseTextStyles, 34 | typography, 35 | variant({ 36 | variants: theme.textStyles 37 | }), 38 | color, 39 | space, 40 | grid, 41 | lineHeight, 42 | system({ 43 | textAlign: { 44 | property: 'textAlign' 45 | } 46 | }) 47 | ) 48 | 49 | Text.defaultProps = { 50 | variant: 'body' 51 | } 52 | 53 | const semanticallyStyledText = (as: string, variant?: keyof typeof theme.textStyles | null) => ( 54 | props: React.ComponentProps 55 | ) => ( 56 | 57 | {props.children} 58 | 59 | ) 60 | 61 | // Custom text variants with styles & semantically correct tagnames baked in 62 | export const H1 = semanticallyStyledText('h1', 'h1') 63 | export const BrandHeader = H1 64 | 65 | export const H2 = semanticallyStyledText('h2', 'h2') 66 | export const PageHeader = H2 67 | 68 | export const H3 = semanticallyStyledText('h3', 'h3') 69 | export const SectionHeader = H3 70 | 71 | export const H4 = semanticallyStyledText('h4', 'h4') 72 | export const CardHeader = H4 73 | 74 | export const H5 = semanticallyStyledText('h5', 'h5') 75 | export const SectionSubheader = H5 76 | 77 | export const H6 = semanticallyStyledText('h6', 'h6') 78 | export const MainNav = H6 79 | 80 | export const Body = semanticallyStyledText('p', 'body') 81 | export const Body2 = semanticallyStyledText('p', 'body2') 82 | export const Body3 = semanticallyStyledText('p', 'body3') 83 | 84 | export const Label = semanticallyStyledText('p', 'label') 85 | export const Plain = semanticallyStyledText('p', null) 86 | export const Custom = Plain 87 | -------------------------------------------------------------------------------- /src/docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Schedule Selector 5 | 6 | 7 | 8 | 12 | 13 | 14 |
15 |
16 |
17 | 18 | -------------------------------------------------------------------------------- /src/docs/styles/animations.ts: -------------------------------------------------------------------------------- 1 | import { keyframes } from '@emotion/core' 2 | 3 | export const fadeIn = keyframes` 4 | from { opacity: 0; } 5 | to { opacity: 1; } 6 | ` 7 | 8 | export const growIn = keyframes` 9 | from { transform: scale(0); } 10 | to { transform: scale(1); } 11 | ` 12 | 13 | export const rotate360 = keyframes` 14 | from { transform: rotate(0deg); } 15 | to { transform: rotate(360deg); } 16 | ` 17 | 18 | // A bit confusing, sourced from https://codepen.io/anon/pen/bwmkAo?editors=1100 19 | // This and the rotateShow keyframes below are used to animate flipping a card over 20 | export const rotateNoShow = keyframes` 21 | 0% { 22 | transform: rotateY(0deg); 23 | height: 100%; 24 | width: 100%; 25 | } 26 | 27 | 49% { 28 | height: 100%; 29 | width: 100%; 30 | } 31 | 32 | 50% { 33 | height: 0; 34 | width: 0; 35 | } 36 | 37 | 100% { 38 | transform: rotateY(180deg); 39 | height: 0; 40 | width: 0; 41 | } 42 | ` 43 | 44 | export const rotateShow = keyframes` 45 | 0% { 46 | transform: rotateY(-180deg); 47 | height: 0; 48 | width: 0; 49 | } 50 | 51 | 49% { 52 | height: 0; 53 | width: 0; 54 | } 55 | 56 | 50% { 57 | height: 100%; 58 | width: 100%; 59 | } 60 | 61 | 100% { 62 | transform: rotateY(0deg); 63 | height: 100%; 64 | width: 100%; 65 | } 66 | ` 67 | -------------------------------------------------------------------------------- /src/docs/styles/breakpoints.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | // Unfortunately, I couldn't figure out how to type an array that would also allow string indexes so I'm typing it as any for now 4 | const breakpoints: any = ['20em', '38em', '64em'] 5 | const mq = breakpoints.map((bp: any) => `@media (min-width: ${bp})`) 6 | breakpoints.sm = breakpoints[0] 7 | mq.sm = mq[0] 8 | breakpoints.md = breakpoints[1] 9 | mq.md = mq[1] 10 | breakpoints.lg = breakpoints[2] 11 | mq.lg = mq[2] 12 | 13 | export { mq, breakpoints as default } 14 | 15 | export const useBreakpoint = (bp: 'sm' | 'md' | 'lg') => { 16 | const matcher = window.matchMedia(mq[bp].replace('@media ', '')) 17 | const [matches, setMatches] = React.useState(matcher.matches) 18 | React.useEffect(() => { 19 | matcher.addListener((e) => { 20 | if (e.matches) { 21 | setMatches(true) 22 | } else { 23 | setMatches(false) 24 | } 25 | }) 26 | }, [matcher]) 27 | return matches 28 | } 29 | -------------------------------------------------------------------------------- /src/docs/styles/colors.ts: -------------------------------------------------------------------------------- 1 | // Some colors appear more than once to provide useful alternate aliases 2 | const colors = { 3 | // Primary 4 | nomusBlue: '#295689', 5 | primaryBlue: '#295689', 6 | gold: '#EEB941', 7 | 8 | // Secondary 9 | twilight: '#14355A', 10 | secondaryBlue: '#14355A', 11 | brightCoral: '#FF7057', 12 | cyanProcess: '#02ABE8', 13 | 14 | // Text 15 | midnightGray: '#444444', 16 | africanElephant: '#A9A591', 17 | 18 | // Some grays 19 | superlightGray: '#E5E5E5', 20 | white: '#FFFFFF', 21 | 22 | // States 23 | linkBlue: '#3C98C0', 24 | invalidRed: '#B91600', 25 | validGreen: '#62AD00', 26 | disabledBlue: '#8598AD', 27 | 28 | // Other Colors 29 | poppy: '#EA9C00', 30 | 31 | // Backgrounds 32 | ivory: '#FBF9F0', 33 | offWhite: '#FDFCF7', 34 | 35 | // Buttons 36 | hoverBlue: '#F0F7FF', 37 | activeBlue: '#E5F1FF', 38 | outlineBlue: '#B2D6FF', 39 | 40 | hoverRed: '#FFF0F7', 41 | activeRed: '#FFE5F1', 42 | outlineRed: '#FFB2D6', 43 | 44 | hoverGreen: '#F0FFF7', 45 | activeGreen: '#E5FFF1', 46 | outlineGreen: '#B2FFD6', 47 | } as const 48 | 49 | export default colors 50 | -------------------------------------------------------------------------------- /src/docs/styles/components/buttonlike.ts: -------------------------------------------------------------------------------- 1 | import colors from '../colors' 2 | import typography from '../typography' 3 | import * as polished from 'polished' 4 | const { fontFamilies } = typography 5 | 6 | // Defining button styles centrally here since both and