a simple demo of redux-form's FieldArrays, validations, formatting, and parsing
based on designs by @sbelous
A Pen by not important on CodePen.
| <div id="js-app"></div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.20.0/polyfill.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.2/react-dom.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.5.2/redux.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.4.5/react-redux.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/redux-saga/0.14.2/redux-saga.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/redux-form/6.4.3/redux-form.js"></script> | |
| <script src="https://unpkg.com/react-router/umd/ReactRouter.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react-router-redux/4.0.7/ReactRouterRedux.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/dedupe.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script> <!-- this is just for the credit card validation library, not sure why it even needs jQuery --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.payment/3.0.0/jquery.payment.js"></script> | |
| <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/redux-devtools.js"></script> | |
| <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/redux-devtools-dock-monitor.js"></script> | |
| <script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/redux-devtools-log-monitor.js"></script> | |
| <script src="//codepen.io/clindsey/pen/LbyNre.js"></script> |
a simple demo of redux-form's FieldArrays, validations, formatting, and parsing
based on designs by @sbelous
A Pen by not important on CodePen.
| const { | |
| call, | |
| takeEvery | |
| } = ReduxSaga.effects; | |
| const { | |
| routerMiddleware, | |
| routerReducer, | |
| syncHistoryWithStore | |
| } = ReactRouterRedux; | |
| const { | |
| Field, | |
| FieldArray, | |
| Fields, | |
| FormSection, | |
| reduxForm | |
| } = ReduxForm; | |
| const { | |
| IndexRoute, | |
| Link, | |
| Route, | |
| Router, | |
| hashHistory | |
| } = ReactRouter; | |
| const DockMonitor = ReduxDevToolsDockMonitor.default; | |
| const LogMonitor = ReduxDevToolsLogMonitor.default; | |
| setTimeout(() => { | |
| const {Provider} = ReactRedux; | |
| const store = configureStore(); | |
| const history = syncHistoryWithStore(hashHistory, store); | |
| ReactDOM.render(( | |
| <Provider {...{store}}> | |
| <Router {...{history}}> | |
| <Route | |
| component={AppContainer} | |
| path="/" | |
| > | |
| <Route | |
| component={CreateContainer} | |
| path="create" | |
| /> | |
| <Route | |
| component={EditContainer} | |
| path="edit/:id" | |
| /> | |
| <IndexRoute component={ListContainer} /> | |
| </Route> | |
| </Router> | |
| </Provider> | |
| ), document.getElementById('js-app')); | |
| }, 0); | |
| function configureStore (initialState) { | |
| const reducers = Redux.combineReducers({ | |
| form: ReduxForm.reducer, | |
| routing: routerReducer, | |
| records: recordsReducer | |
| }); | |
| const router = routerMiddleware(hashHistory); | |
| const sagaMiddleware = ReduxSaga.default(); | |
| const enhancer = Redux.compose(Redux.applyMiddleware(router, sagaMiddleware), DevTools.instrument()); | |
| const store = Redux.createStore(reducers, initialState, enhancer); | |
| sagaMiddleware.run(rootSaga); | |
| return store; | |
| }; | |
| function *rootSaga () { | |
| yield [ | |
| createRedirect(), | |
| updateRedirect() | |
| ]; | |
| } | |
| function *createRedirect () { | |
| yield takeEvery('RECORD_ADD', listRedirectEffect) | |
| } | |
| function *updateRedirect () { | |
| yield takeEvery('RECORD_UPDATE', listRedirectEffect) | |
| } | |
| function *listRedirectEffect () { | |
| yield call(hashHistory.push, '/'); | |
| } | |
| function recordsReducer (state = {maxId: 0, records: {}}, action) { | |
| if (action.type === 'RECORD_ADD') { | |
| return { | |
| ...state, | |
| maxId: state.maxId + 1, | |
| records: { | |
| ...state.records, | |
| [state.maxId]: action.record | |
| } | |
| }; | |
| } | |
| if (action.type === 'RECORD_UPDATE') { | |
| return { | |
| ...state, | |
| records: { | |
| ...state.records, | |
| [action.id]: action.record | |
| } | |
| }; | |
| } | |
| return state; | |
| } | |
| const recordsActions = { | |
| addRecord: record => ({type: 'RECORD_ADD', record}), | |
| updateRecord: (id, record) => ({type: 'RECORD_UPDATE', id, record}) | |
| }; | |
| class AppComponent extends React.Component { | |
| render () { | |
| return ( | |
| <div className="o-wrapper"> | |
| {this.props.children} | |
| <DevTools /> | |
| </div> | |
| ); | |
| } | |
| } | |
| const AppContainer = ReactRedux.connect(() => { | |
| return { | |
| }; | |
| }, { | |
| })(AppComponent); | |
| class CreateComponent extends React.Component { | |
| static propTypes = { | |
| addRecord: React.PropTypes.func.isRequired | |
| }; | |
| constructor (props) { | |
| super(props); | |
| this.handleSubmit = this.onSubmit(); | |
| } | |
| onSubmit () { | |
| return values => { | |
| this.props.addRecord(values); | |
| }; | |
| } | |
| render () { | |
| const initialValues = {}; | |
| return ( | |
| <div className="o-layout"> | |
| <div className="o-layout__item u-1/4@tablet"> | |
| </div> | |
| <div className="o-layout__item u-1/2@tablet"> | |
| <h1 className="c-h1">{'New Person'}</h1> | |
| <CreateForm | |
| onSubmit={this.handleSubmit} | |
| {...{initialValues}} | |
| /> | |
| <Link to="/">{'List'}</Link> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| } | |
| const CreateContainer = ReactRedux.connect(() => { | |
| return { | |
| }; | |
| }, { | |
| addRecord: recordsActions.addRecord | |
| })(CreateComponent); | |
| class EditComponent extends React.Component { | |
| static propTypes = { | |
| id: React.PropTypes.string.isRequired, | |
| record: React.PropTypes.object.isRequired, | |
| updateRecord: React.PropTypes.func.isRequired | |
| }; | |
| constructor (props) { | |
| super(props); | |
| this.handleSubmit = this.onSubmit(); | |
| } | |
| onSubmit () { | |
| const {id} = this.props; | |
| return values => { | |
| this.props.updateRecord(id, values); | |
| }; | |
| } | |
| render () { | |
| const {record} = this.props; | |
| const initialValues = record; | |
| return ( | |
| <div className="o-layout"> | |
| <div className="o-layout__item u-1/4@tablet"> | |
| </div> | |
| <div className="o-layout__item u-1/2@tablet"> | |
| <h1 className="c-h1">{'Update Person'}</h1> | |
| <EditForm | |
| onSubmit={this.handleSubmit} | |
| {...{initialValues}} | |
| /> | |
| <Link to="/">{'List'}</Link> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| } | |
| const EditContainer = ReactRedux.connect((state, ownProps) => { | |
| const { | |
| params: { | |
| id | |
| } | |
| } = ownProps; | |
| const { | |
| records: { | |
| records | |
| } | |
| } = state; | |
| return { | |
| id, | |
| record: records[id] | |
| }; | |
| }, { | |
| updateRecord: recordsActions.updateRecord | |
| })(EditComponent); | |
| class ListComponent extends React.Component { | |
| render () { | |
| const {records} = this.props; | |
| return ( | |
| <div className="o-layout"> | |
| <div className="o-layout__item u-1/4@tablet"> | |
| </div> | |
| <div className="o-layout__item u-1/2@tablet"> | |
| <h1 className="c-h1">{'People List'}</h1> | |
| <ul className="o-list-bare"> | |
| {Object.keys(records).map((recordKey, key) => { | |
| const record = records[recordKey]; | |
| return ( | |
| <li {...{key}}> | |
| <table className="o-table"> | |
| <tbody> | |
| <tr> | |
| <td colSpan="3"><Link to={`/edit/${recordKey}`}>{record.profile.firstLastName}</Link></td> | |
| </tr> | |
| <tr> | |
| <td colSpan="3">{record.profile.phoneNumber}</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </li> | |
| ); | |
| })} | |
| </ul> | |
| <Link to="/create">{'Add a new person'}</Link> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| } | |
| const ListContainer = ReactRedux.connect(state => { | |
| const { | |
| records: { | |
| records | |
| } | |
| } = state; | |
| return { | |
| records | |
| }; | |
| }, { | |
| })(ListComponent); | |
| class PersonForm extends React.Component { | |
| render () { | |
| const { | |
| handleSubmit, | |
| invalid | |
| } = this.props; | |
| return ( | |
| <form onSubmit={handleSubmit}> | |
| <FormSection name="profile"> | |
| <ProfileFields /> | |
| </FormSection> | |
| <RelationsFields /> | |
| <FormButton | |
| className="c-form-button--primary c-form-button--block" | |
| label="Submit" | |
| type="submit" | |
| /> | |
| </form> | |
| ); | |
| } | |
| } | |
| function validateProfile (values) { | |
| const errors = {}; | |
| if (!values || !values.firstLastName) { | |
| errors.firstLastName = 'Required'; | |
| } else if (values.firstLastName.split(' ').length < 2) { | |
| errors.firstLastName = 'Both first and last name are required'; | |
| } | |
| if (!values || !values.phoneNumber) { | |
| errors.phoneNumber = 'Required'; | |
| } else if (!values.phoneNumber.match(/^\d{10}$/)) { | |
| errors.phoneNumber = 'Invalid'; | |
| } | |
| if (!values || !values.type) { | |
| errors.type = 'Required'; | |
| } | |
| return errors; | |
| } | |
| function validateRelations (values) { | |
| if (!values) { | |
| return []; | |
| } | |
| const errors = []; | |
| values.forEach(((relation, index) => { | |
| const memberErrors = validateProfile(relation); | |
| if (Object.keys(memberErrors).length) { | |
| errors[index] = memberErrors; | |
| } | |
| })); | |
| return errors; | |
| } | |
| const validateSignUp = validationEngine({ | |
| relations: validateRelations, | |
| profile: validateProfile | |
| }); | |
| const CreateForm = reduxForm({ | |
| form: 'createForm', | |
| validate: validateSignUp | |
| })(PersonForm); | |
| const EditForm = reduxForm({ | |
| form: 'editForm', | |
| validate: validateSignUp | |
| })(PersonForm); | |
| class ProfileFields extends React.Component { | |
| render () { | |
| return ( | |
| <div> | |
| <Field | |
| component={FormRadio} | |
| label="Type" | |
| name="type" | |
| options={[ | |
| { | |
| classes: 'u-1/3', | |
| icon: 'Password', | |
| label: 'Human', | |
| value: 'human' | |
| }, { | |
| classes: 'u-1/3', | |
| icon: 'Password', | |
| label: 'Alien', | |
| value: 'alien' | |
| }, { | |
| classes: 'u-1/3', | |
| icon: 'Password', | |
| label: 'Cyborg', | |
| value: 'cyborg' | |
| } | |
| ]} | |
| /> | |
| <FormField | |
| fields={[ | |
| { | |
| name: 'firstLastName', | |
| placeholder: 'First and Last Name', | |
| type: 'text' | |
| } | |
| ]} | |
| icon="Password" | |
| /> | |
| <FormField | |
| fields={[ | |
| { | |
| name: 'phoneNumber', | |
| format: formatPhone, | |
| parse: parsePhone, | |
| placeholder: 'Phone Number', | |
| type: 'tel' | |
| } | |
| ]} | |
| icon="Password" | |
| /> | |
| </div> | |
| ); | |
| } | |
| } | |
| class RelationsFields extends React.Component { | |
| renderRelations ({fields}) { | |
| return ( | |
| <ul className="o-list-bare"> | |
| {fields.map((member, key) => ( | |
| <li | |
| className="u-margin-bottom" | |
| {...{key}} | |
| > | |
| <div className="o-media"> | |
| <div className="o-media__img"> | |
| <FormButton | |
| className="c-form-button--destructive" | |
| label="X" | |
| onClick={() => fields.remove(key)} | |
| type="button" | |
| /> | |
| </div> | |
| <div className="o-media__body"> | |
| <h4 className="c-h4 u-margin-bottom-small">{`Relation ${key + 1}`}</h4> | |
| <FormSection name={member}> | |
| <ProfileFields /> | |
| </FormSection> | |
| </div> | |
| </div> | |
| </li> | |
| ))} | |
| <li> | |
| <FormButton | |
| className="c-form-button--primary c-form-button--inverse c-form-button--block" | |
| label="+ Add a relation" | |
| onClick={() => fields.push({})} | |
| type="button" | |
| /> | |
| </li> | |
| </ul> | |
| ); | |
| } | |
| render () { | |
| return ( | |
| <FieldArray | |
| name="relations" | |
| component={this.renderRelations} | |
| /> | |
| ); | |
| } | |
| } | |
| class FormField extends React.Component { // refactor, missing propTypes | |
| static propTypes = { | |
| icon: React.PropTypes.string, | |
| fields: React.PropTypes.arrayOf(React.PropTypes.shape({ | |
| className: React.PropTypes.string, | |
| format: React.PropTypes.func, | |
| name: React.PropTypes.string.isRequired, | |
| normalize: React.PropTypes.func, | |
| parse: React.PropTypes.func, | |
| placeholder: React.PropTypes.string.isRequired, // refactor, is this really required? Is this used for making checkboxes/radios? | |
| type: React.PropTypes.string.isRequired | |
| })), | |
| hint: React.PropTypes.string | |
| }; | |
| renderFields (props) { | |
| const { | |
| fields, | |
| hint, | |
| icon | |
| } = props; | |
| const errors = fields.map(({name}) => { | |
| const { | |
| meta: { | |
| error, | |
| touched | |
| } // refactor, ¯\_(ツ)_/¯ @ next line | |
| } = eval(`props.${name}`); // eslint-disable-line no-eval | |
| return touched && error; // refactor, `touched` might not be behaving as expected? | |
| }).filter(i => i); | |
| const error = errors[0] || props.error; | |
| const message = error || hint; | |
| const className = classNames({ | |
| 'c-form-field': true, | |
| 'c-form-field--error': !!error | |
| }); | |
| const Icon = SVGIcon[icon]; | |
| return ( | |
| <div {...{className}}> | |
| <div className="o-media"> | |
| <div className="o-media__img c-form-field__img"> | |
| {icon && (<Icon />)} | |
| </div> | |
| <div className="o-media__body"> | |
| <div className="c-form-field__control"> | |
| {fields.map((field, key) => { | |
| const { | |
| format, | |
| name, | |
| normalize, | |
| parse, | |
| placeholder, | |
| type | |
| } = field; // refactor, what else is in `props.${name}`? | |
| // refactor, I had to comment out the line below to make this work with FormSection | |
| // const {input} = eval(`props.${name}`); // eslint-disable-line no-eval | |
| const inputClassName = classNames({ | |
| 'c-form-field__input': true, | |
| [field.className]: field.className && true // refactor, `&& true`? I don't get it | |
| }); | |
| return ( | |
| <Field | |
| className={inputClassName} | |
| component="input" | |
| {...{key, placeholder, type, format, normalize, parse, name}} | |
| /> | |
| ); | |
| })} | |
| </div> | |
| <div className="c-form-field__hint">{message}</div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| render () { | |
| const { | |
| fields, | |
| hint, | |
| icon | |
| } = this.props; | |
| return ( | |
| <Fields | |
| component={this.renderFields} | |
| names={fields.map(({name}) => name)} | |
| {...{fields, hint, icon}} | |
| /> | |
| ); | |
| } | |
| } | |
| class FormRadio extends React.Component { | |
| handleChange (value) { | |
| return () => { | |
| this.props.input.onChange(value); | |
| } | |
| } | |
| render () { | |
| const { | |
| hint, | |
| input: { | |
| value, | |
| name | |
| }, | |
| meta: { | |
| error, | |
| touched | |
| }, | |
| options | |
| } = this.props; | |
| const message = (touched && error) || hint; | |
| const className = classNames({ | |
| 'c-form-radio': true, | |
| 'c-form-radio--error': (touched && !!error) | |
| }); | |
| return ( | |
| <div {...{className}}> | |
| <div className="o-layout"> | |
| {options.map((field, key) => { | |
| const Icon = SVGIcon[field.icon]; | |
| const fieldClasses = field.classes || ''; | |
| return ( | |
| <div | |
| className={`c-form-radio__item o-layout__item ${fieldClasses}`} | |
| {...{key}} | |
| > | |
| <input | |
| checked={value === field.value} | |
| className="c-form-radio__field u-hidden-visually" | |
| id={`${name}-${key}`} | |
| onChange={this.handleChange(field.value)} | |
| type="radio" | |
| value={field.value} | |
| {...{name}} | |
| /> | |
| <label | |
| className="c-form-radio__label" | |
| htmlFor={`${name}-${key}`} | |
| > | |
| {field.icon && Icon && (<Icon active={value === field.value} />)} | |
| {field.label} | |
| </label> | |
| </div> | |
| ); | |
| })} | |
| <div className="o-layout__item u-1/1 c-form-radio__message"> | |
| <div className="c-form-radio__hint">{message}</div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| } | |
| class FormButton extends React.Component { | |
| static propTypes = { | |
| className: React.PropTypes.string, | |
| disabled: React.PropTypes.bool, | |
| onClick: React.PropTypes.func, | |
| type: React.PropTypes.string.isRequired | |
| }; | |
| render () { | |
| const { | |
| disabled, | |
| label, | |
| onClick, | |
| type | |
| } = this.props; | |
| const className = classNames({ | |
| 'c-form-button': true, | |
| [this.props.className]: this.props.className && true | |
| }); | |
| return ( | |
| <button {...{ | |
| className, | |
| disabled, | |
| onClick, | |
| type | |
| }} | |
| >{label}</button> | |
| ); | |
| } | |
| } | |
| const DevTools = ReduxDevTools.createDevTools( | |
| <DockMonitor | |
| changePositionKey="ctrl-q" | |
| defaultIsVisible={false} | |
| toggleVisibilityKey="ctrl-h" | |
| > | |
| <LogMonitor theme="tomorrow" /> | |
| </DockMonitor> | |
| ); | |
| function formatPhone (input) { // refactor, deleting a phone number doesn't work so well | |
| if (!input) { | |
| return ''; | |
| } // logic taken from https://github.com/omarshammas/jquery.formance/ | |
| const results = input.replace(/\D/g, '').match(/^(\d{0,3})?(\d{0,3})?(\d{0,4})?$/); | |
| if (!results) { | |
| return input; | |
| } | |
| const [_phoneNumber, areaCode, first3, last4] = results.filter(i => i).map(i => `${i}`); // eslint-disable-line no-unused-vars | |
| let output = ''; | |
| if (areaCode) { | |
| output += `(${areaCode}`; | |
| } | |
| if (areaCode && areaCode.length === 3) { | |
| output += ') '; | |
| } | |
| if (first3) { | |
| output += first3; | |
| } | |
| if (first3 && first3.length === 3) { | |
| output += ' - '; | |
| } | |
| if (last4) { | |
| output += last4; | |
| } | |
| return output; | |
| } | |
| function parsePhone (input) { | |
| if (/\d/.test(input) === false) { | |
| return input; | |
| } | |
| return input.match(/\d+/g).join(''); | |
| } | |
| function formatExpirationDate (input) { // refactor, doesn't work very well when deleting | |
| if (!input) { | |
| return ''; | |
| } // logic taken from https://github.com/omarshammas/jquery.formance/ | |
| let output = input; | |
| if (/^\d$/.test(input) && input !== '0' && input !== '1') { | |
| output = `0${output} / `; | |
| } else if (/^\d\d$/.test(input)) { | |
| output = `${output} / `; | |
| } | |
| return output; | |
| } | |
| function validationEngine (validators) { | |
| return values => { | |
| return Object.keys(validators).map(name => ({ | |
| name, // 5625463739 | |
| error: validators[name](values[name]) | |
| })).reduce((p, {name, error}) => ( | |
| Object.keys(name).length ? {...p, [name]: error} : p | |
| ), {}); | |
| }; | |
| } |
| // begin colors | |
| $brand-primary: #60c1da; | |
| $brand-secondary: #0698bd; | |
| $brand-red: #e90c27; | |
| $brand-green: #4ab043; | |
| $brand-yellow: #ffd305; | |
| $brand-black: #4a4a4a; | |
| $brand-gray: #9b9b9b; | |
| $brand-lighter-gray: #b0b0b0; | |
| $brand-disabled-gray: #d0d0d0; | |
| $brand-border-gray: #e0e0e0; | |
| $brand-primary-muted: #a0dae9; | |
| $brand-success: #5cb85c; | |
| $brand-info: #5bc0de; // stylelint-disable-line no-indistinguishable-colors | |
| $brand-warning: #f0ad4e; | |
| $brand-danger: #d9534f; | |
| $brand-inverse: $gray-dark; | |
| // end colors | |
| html { | |
| color: $text-base-color; | |
| font-family: 'Open Sans', sans-serif; | |
| font-size: 14px; | |
| @include mq($from: tablet) { | |
| font-size: 16px; | |
| } | |
| } | |
| body { | |
| margin-top: $inuit-global-spacing-unit; | |
| margin-bottom: $inuit-global-spacing-unit; | |
| } | |
| // begin FormField | |
| .c-form-field { | |
| margin-bottom: $inuit-global-spacing-unit; | |
| } | |
| .c-form-field--error { | |
| .c-form-field__control { | |
| border-bottom: solid 1px $border-color; | |
| } | |
| .c-form-field__hint { | |
| color: $brand-danger; | |
| } | |
| } | |
| .c-form-field__hint { | |
| font-size: 0.75rem; | |
| color: $text-muted-color; | |
| font-weight: 300; | |
| min-height: $inuit-global-spacing-unit; | |
| } | |
| .c-form-field__img { | |
| height: 1.375rem; // 22px | |
| margin-right: 0; | |
| text-align: center; | |
| width: 2.125rem; // 34px | |
| > img { | |
| margin: 0 auto; | |
| height: 1.375rem; // 22px | |
| } | |
| } | |
| .c-form-field__control { | |
| border-bottom: solid 1px $border-color; | |
| } | |
| .c-form-field__input { | |
| -webkit-backface-visibility: hidden; | |
| border: none; | |
| color: $text-base-color; | |
| font-family: inherit; | |
| font-weight: 300; | |
| padding: 0; | |
| width: 100%; | |
| } | |
| // end FormField | |
| // begin FormButton | |
| .c-form-button { | |
| border: solid 1px $white; | |
| border-radius: $control-radius; | |
| color: $white; | |
| cursor: pointer; | |
| display: inline-block; | |
| font: inherit; | |
| font-weight: 600; | |
| margin: $inuit-global-spacing-unit-small 0; | |
| padding: round($inuit-global-spacing-unit-small * 0.5) $inuit-global-spacing-unit; | |
| text-align: center; | |
| vertical-align: middle; | |
| } | |
| .c-form-button--primary { | |
| background-color: $brand-primary; | |
| border-color: $brand-primary; | |
| &.c-form-button--disabled, | |
| &:disabled { | |
| background-color: $brand-primary-muted; | |
| border-color: $brand-primary-muted; | |
| } | |
| &.c-form-button--inverse { | |
| background-color: $white; | |
| color: $brand-primary; | |
| border-color: $brand-primary; | |
| } | |
| } | |
| .c-form-button--block { | |
| width: 100%; | |
| } | |
| .c-form-button--destructive { | |
| background-color: $brand-danger; | |
| border-color: $brand-danger; | |
| } | |
| // end FormButton | |
| // begin FormRadio | |
| .c-form-radio__label { | |
| border-radius: $control-radius; | |
| border: solid 1px $border-color; | |
| color: $text-muted-color; | |
| cursor: pointer; | |
| display: block; | |
| margin: 0 auto; | |
| overflow-x: hidden; | |
| padding: $inuit-global-spacing-unit-tiny; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| width: 100%; | |
| } | |
| .c-form-radio__item { | |
| margin-bottom: $inuit-global-spacing-unit-tiny; | |
| text-align: center; | |
| } | |
| .c-form-radio__field:checked + label { | |
| border-color: $brand-primary; | |
| color: $brand-primary; | |
| } | |
| .c-form-radio--error { | |
| .c-form-radio__hint { | |
| color: $brand-danger; | |
| } | |
| } | |
| .c-form-radio__hint { | |
| color: $text-muted-color; | |
| font-size: 0.75rem; | |
| min-height: 2rem; | |
| } | |
| // end FormRadio | |
| // begin Typography | |
| .c-h1 { | |
| font-size: 2rem; | |
| } | |
| .c-h2 { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| } | |
| .c-h3 { | |
| font-size: 1.25rem; | |
| } | |
| .c-h4 { | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| } | |
| .c-h5 { | |
| font-size: 0.9375rem; | |
| font-weight: 600; | |
| } | |
| .c-text-strong { | |
| color: $brand-secondary; | |
| } | |
| .c-text-strong--inverse { | |
| color: $white; | |
| } | |
| .c-text-strong--stronger { | |
| font-weight: 600; | |
| color: $text-base-color; | |
| } | |
| .c-text-strong--super-strong { | |
| font-weight: 600; | |
| font-size: 1.25rem; | |
| } | |
| .c-text-small { | |
| font-size: 0.875rem; | |
| } | |
| .c-text-small--muted { | |
| color: $brand-gray; | |
| } | |
| .c-text-small--strong { | |
| color: $brand-secondary; | |
| } | |
| .c-text-small--stronger { | |
| font-weight: 600; | |
| } | |
| // end Typography |
| <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,600" rel="stylesheet" /> | |
| <link href="http://codepen.io/clindsey/pen/XNKwXY" rel="stylesheet" /> | |
| <link href="http://codepen.io/clindsey/pen/jVMoKL" rel="stylesheet" /> | |
| <link href="http://codepen.io/clindsey/pen/LbyNre" rel="stylesheet" /> |